
Ilya Nekrasov, Mahtalitet , desenvolvedor Android do KODE
Por dois anos e meio no desenvolvimento do Android, consegui trabalhar em projetos completamente diferentes: de uma rede social para motoristas e um banco da Letônia a um sistema federal de bônus e a terceira companhia aérea de transporte. De qualquer forma, em cada um desses projetos, deparei-me com tarefas que exigiam a busca de soluções não clássicas ao implementar listas usando a classe RecyclerView.
Este artigo - fruto da preparação para uma apresentação no DevFest Kaliningrad'18, bem como da comunicação com colegas - será especialmente útil para desenvolvedores iniciantes e para aqueles que usaram apenas uma das bibliotecas existentes.
Para começar, investigamos um pouco a essência da questão e a fonte da dor, a saber, o crescimento da funcionalidade no desenvolvimento do aplicativo e a complexidade das listas usadas.
Capítulo um, no qual o cliente sonha com um aplicativo, e nós - sobre requisitos claros
Vamos imaginar uma situação em que um cliente entra em contato com um estúdio que deseja um aplicativo móvel para uma loja de patos de borracha.
O projeto está se desenvolvendo rapidamente, novas idéias surgem regularmente e não são enquadradas em um roteiro de longo prazo.
Primeiro, o cliente solicita que mostremos uma lista de mercadorias existentes e, quando clicadas, preenche uma solicitação de entrega. Você não precisa ir muito longe para encontrar uma solução: usamos o conjunto clássico do RecyclerView , um simples adaptador auto-escrito para ele e Activity .
Para o adaptador, usamos dados homogêneos, um ViewHolder e lógica de ligação simples.
Adaptador com patosclass DucksClassicAdapter( private val data: List<Duck>, private val onDuckClickAction: (Duck) -> Unit ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(rubberDuckView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val duck = data[position] holder.divider.isVisible = position != 0 holder.rubberDuckImage.apply { Picasso.get() .load(duck.icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(R.drawable.duck_stub) .into(this) } holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) } } override fun getItemCount() = data.count() class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val clicksHolder: View = view.findViewById(R.id.clicksHolder) val divider: View = view.findViewById(R.id.divider) } }
Com o tempo, o cliente teve a ideia de adicionar outra categoria de mercadorias aos patos de borracha, o que significa que ele precisará adicionar um novo modelo de dados e um novo layout. Mais importante, porém, outro ViewType aparecerá no adaptador, com o qual você pode determinar qual ViewHolder usar para um item de lista específico.
Depois disso, os cabeçalhos são adicionados às categorias, segundo as quais cada categoria pode ser recolhida e expandida para simplificar a orientação dos usuários na loja. Isso é mais outro ViewType e ViewHolder para cabeçalhos. Além disso, você terá que complicar o adaptador, pois você precisa manter uma lista de grupos abertos e usá-lo para verificar a necessidade de ocultar e mostrar este ou aquele elemento clicando no cabeçalho.
Adaptador com tudo class DucksClassicAdapter( private val onDuckClickAction: (Pair<Duck, Int>) -> Unit, private val onSlipperClickAction: (Duck) -> Unit, private val onAdvertClickAction: (Advert) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var data: List<Duck> = emptyList() set(value) { field = value internalData = data.groupBy { it.javaClass.kotlin } .flatMap { groupedDucks -> val titleRes = when (groupedDucks.key) { DuckSlipper::class -> R.string.slippers RubberDuck::class -> R.string.rubber_ducks else -> R.string.mixed_ducks } groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) } } .toMutableList() duckCountsAdapters = internalData.map { duck -> val rubberDuck = (duck as? RubberDuck) DucksCountAdapter( data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count }, onCountClickAction = { onDuckClickAction.invoke(it) } ) } } private val advert = DuckMockData.adverts.orEmpty().shuffled().first() private var internalData: MutableList<Duck> = mutableListOf() private var duckCountsAdapters: List<DucksCountAdapter> = emptyList() private var collapsedHeaders: MutableSet<Duck> = hashSetOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_RUBBER_DUCK -> { val view = parent.context.inflate(R.layout.item_rubber_duck, parent) DuckViewHolder(view) } VIEW_TYPE_SLIPPER_DUCK -> { val view = parent.context.inflate(R.layout.item_duck_slipper, parent) SlipperViewHolder(view) } VIEW_TYPE_HEADER -> { val view = parent.context.inflate(R.layout.item_header, parent) HeaderViewHolder(view) } VIEW_TYPE_ADVERT -> { val view = parent.context.inflate(R.layout.item_advert, parent) AdvertViewHolder(view) } else -> throw UnsupportedOperationException("view type $viewType without ViewHolder") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> bindHeaderViewHolder(holder, position) is DuckViewHolder -> bindDuckViewHolder(holder, position) is SlipperViewHolder -> bindSlipperViewHolder(holder, position) is AdvertViewHolder -> bindAdvertViewHolder(holder) } } private fun bindAdvertViewHolder(holder: AdvertViewHolder) { holder.advertImage.showIcon(advert.icon) holder.advertTagline.text = advert.tagline holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) } } private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = getItem(position) as FakeDuck holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) } val arrowRes = if (collapsedHeaders.contains(item)) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp holder.arrow.setImageResource(arrowRes) holder.title.setText(item.titleRes) } private fun changeCollapseState(item: FakeDuck, position: Int) { val isCollapsed = collapsedHeaders.contains(item) if (isCollapsed) { collapsedHeaders.remove(item) } else { collapsedHeaders.add(item) }
Eu acho que você entendeu o ponto - uma pilha de pouco se assemelha a um desenvolvimento saudável. E à frente, há cada vez mais novos requisitos do cliente: fixar um banner de publicidade no topo da lista, perceber a possibilidade de escolher o número de patos pedidos. Somente essas tarefas acabarão se transformando em adaptadores regulares, que novamente terão que ser gravados do zero.
O processo de desenvolvimento de um adaptador clássico na história do github
Sumário
De fato, o quadro não é nada animador: os adaptadores individuais precisam ser aprimorados para casos específicos. Todos nós entendemos que em um aplicativo real existem dezenas ou mesmo centenas dessas telas de lista. E eles não contêm informações sobre patos, mas dados mais complexos. Sim, e seu design é muito mais complicado.
O que há de errado com nossos adaptadores?
- obviamente, eles são difíceis de reutilizar;
- dentro da lógica de negócios aparece e com o tempo se torna cada vez mais;
- difícil de manter e expandir;
- alto risco de erros ao atualizar dados;
- design não óbvio.
Capítulo dois, em que tudo poderia ser diferente
Não é realista e sem sentido imaginar o desenvolvimento do aplicativo nos próximos anos. Depois de algumas danças com um pandeiro, como no capítulo anterior, e escrevendo dezenas de adaptadores, qualquer pessoa terá uma pergunta: "Talvez haja outras soluções?".

Depois de concluir o Github, descobrimos que a primeira biblioteca AdapterDelegates apareceu em 2015 e, um ano depois, o Groupie e o Epoxy adicionaram ao arsenal de desenvolvedores - todos eles ajudam a tornar a vida mais fácil, mas cada um tem suas próprias especificidades e armadilhas.
Existem várias bibliotecas semelhantes (por exemplo, FastAdapter), mas nem eu nem meus colegas trabalhamos com elas, portanto, não as consideraremos no artigo.
Antes de comparar as bibliotecas, analisamos brevemente o caso descrito acima com a loja on-line com a condição de usar o AdapterDelegates - das bibliotecas desmontadas, é o mais simples do ponto de vista da implementação e uso interno (no entanto, não é avançado em tudo, é preciso adicionar muitas coisas manualmente).
A biblioteca não nos salvará completamente do adaptador, mas será formada a partir de blocos (tijolos), que podem ser adicionados ou removidos com segurança da lista e trocados.
Adaptador de bloco class DucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() { init { delegatesManager.addDelegate(RubberDuckDelegate()) } fun setData(items: List<DisplayableItem>) { this.items = items notifyDataSetChanged() } } private class RubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() { override fun isForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean { return item is RubberDuckItem } override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(item) } override fun onBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) { viewHolder.apply { rubberDuckImage.showIcon(item.icon) } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage) } }
Usando um Adaptador de Atividade class DucksDelegatesActivity : AppCompatActivity() { private lateinit var ducksList: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ducks_delegates) ducksList = findViewById(R.id.duckList) ducksList.apply { layoutManager = LinearLayoutManager(this@DucksDelegatesActivity) adapter = createAdapter().apply { showData() } } } fun createAdapter(): DucksDelegatesAdapter { return DucksDelegatesAdapter() } private fun DucksDelegatesAdapter.showData() { setData(getRubberDucks()) } private fun getRubberDucks(): List<DisplayableItem> { return DuckMockData.ducks.orEmpty().map { RubberDuckItem(it.icon) } } }
Desde a primeira tarefa, vemos a diferença: temos uma classe de adaptador que herda da biblioteca. Além disso - o mesmo bloco, que é chamado de delegado e do qual também herdamos e implementamos parte da lógica de que precisamos. Em seguida, adicionamos um delegado ao gerente - essa também é uma classe de biblioteca. E a última coisa que você precisa é criar um adaptador e preenchê-lo com dados
Para implementar a segunda categoria da loja e os cabeçalhos, escreveremos mais alguns delegados, e a animação aparecerá graças à classe DiffUtil .
Aqui vou indicar uma conclusão breve, mas definitiva: o uso desta biblioteca resolve todos os problemas listados que surgiram quando complicamos o aplicativo em um caso com uma loja online, mas sem os pontos negativos, em nenhum outro lugar.
Processo de desenvolvimento de adaptadores com AdapterDelegates no histórico do github
Capítulo Três, no qual o desenvolvedor remove óculos cor de rosa comparando bibliotecas
Iremos nos aprofundar na funcionalidade e operação de cada uma das bibliotecas. De uma maneira ou de outra, usei as três bibliotecas em nossos projetos, dependendo das tarefas e da complexidade do aplicativo.
Usamos essa biblioteca na aplicação de uma das maiores companhias aéreas russas. Precisávamos substituir uma lista de pagamentos simples por uma lista com grupos e um grande número de parâmetros diferentes.
Um esquema de trabalho simplificado da biblioteca se parece com isso: 
A classe principal é o DelegateAdapter , os vários "tijolos" são os "delegados" responsáveis por exibir um tipo de dados específico e, é claro, a própria lista.
Prós:
- simplicidade de imersão;
- adaptadores fáceis de reutilizar;
- poucos métodos e classes;
- sem reflexão, geração de código e ligação de dados.
Contras:
- você mesmo precisa implementar a lógica, por exemplo
atualizando itens através do DiffUti l (desde a versão 3.1.0, você pode usar o adaptador AsyncListDifferDelegationAdapter); - código redundante.
Em geral, esta biblioteca resolve todas as principais dificuldades na expansão da funcionalidade do aplicativo e é adequada para aqueles que não usaram a biblioteca anteriormente. Mas para parar apenas com isso, eu não aconselho.
Costumamos usar o Groupie , criado há vários anos por Lisa Wray , inclusive escrevendo o aplicativo para um banco letão usando-o completamente.
Para usar esta biblioteca, primeiro você precisa lidar com dependências . Além da principal, você pode usar várias opções para escolher:
Nós nos debruçamos sobre uma coisa e prescrevemos as dependências necessárias.
Usando um exemplo de uma loja on-line com patos, precisamos criar um Item herdado da classe library, especificar o layout e implementar a ligação por meio do Kotlin syntentics. Comparado à quantidade de código que tive que escrever com o AdapterDelegates , é apenas o céu e a terra.
Tudo o que resta é definir o RecyclerView GroupieAdapter como um adaptador e colocar os itens simulados nele.

Pode-se ver que o esquema de trabalho é maior e mais complexo. Aqui, além de itens simples, você pode usar seções inteiras - grupos de itens e outras classes.
Prós:
- interface intuitiva, embora api faça você pensar;
- a presença de soluções in a box;
- decomposição em grupos de elementos;
- a escolha entre a opção usual, Kotlin Extensions e DataBinding;
- incorporando ItemDecoration e animações.
Contras:
- wiki incompleto;
- fraco apoio do mantenedor e da comunidade;
- pequenos bugs que precisavam ser contornados na primeira versão;
- diferenças na linha principal (por enquanto);
- não há suporte para AndroidX (no momento, mas você precisa monitorar o repositório).
É importante que o Groupie, com todas as suas desvantagens, consiga substituir facilmente o AdapterDelegates , especialmente se você planeja criar listas dobráveis do primeiro nível e não deseja escrever muitos clichês.
Implementando a lista de patos com groupie
A última biblioteca que começamos a usar há relativamente pouco tempo é o Epoxy , desenvolvido pelo pessoal do Airbnb . A biblioteca é complexa, mas permite resolver uma grande variedade de tarefas. Os próprios programadores do Airbnb o usam para renderizar telas diretamente do servidor. A cola Epoxy foi útil em um de nossos projetos mais recentes - um pedido de banco em Ecaterimburgo.
Para desenvolver telas, tivemos que trabalhar com diferentes tipos de dados, um grande número de listas. E uma das telas era realmente interminável. E a Epóxi nos ajudou a lidar com isso.
O princípio de operação da biblioteca como um todo é semelhante aos dois anteriores, exceto que, em vez de um adaptador, um EpoxyController é usado para criar a lista, o que permite determinar declarativamente a estrutura do adaptador.

Para conseguir isso, a biblioteca é construída sobre a geração de código. Como funciona - com todas as nuances, é bem descrito no wiki e refletido nas amostras .
Prós:
- modelos para a lista, gerados a partir da visualização usual, com possibilidade de reutilização em telas simples;
- descrição declarativa das telas;
- DataBinding na velocidade máxima - gera modelos diretamente de arquivos de layout;
- basta exibir não apenas listas dos blocos, mas também telas complexas;
- ViewPool compartilhado em Atividade;
- difusão assíncrona pronta para uso (AsyncEpoxyController);
- não há necessidade de vaporizar com listas horizontais.
Contras:
- um monte de classes, processadores, anotações;
- mergulho difícil do zero;
- usa o plugin ButterKnife para gerar arquivos R2 em módulos;
- é muito difícil entender como trabalhar corretamente com retornos de chamada (nós mesmos ainda não o entendemos);
- Há problemas que precisam ser contornados: por exemplo, uma falha com o mesmo ID.
Implementando a lista de patos com epóxi
Sumário
O principal que eu queria transmitir era: não atenda à complexidade que aparece quando você precisa fazer listas complexas e constantemente precisa refazê-las. E isso acontece com muita frequência. E, em princípio, quando eles são implementados, se o projeto está apenas começando, ou você está envolvido na refatoração.
A realidade é que não é necessário complicar a lógica mais uma vez, pensando que algum tipo de abstração será suficiente. Eles são curtos o suficiente E trabalhar com eles não é apenas um prazer, mas também a tentação de transferir parte da lógica para a parte da interface do usuário, que não deveria estar lá. Existem ferramentas que podem ajudá-lo a evitar a maioria dos problemas e você precisa usá-las.
Entendo que, para muitos desenvolvedores experientes (e não apenas), isso é óbvio ou eles podem discordar de mim. Mas considero importante enfatizar isso novamente.
Então, o que escolher?
O aconselhamento para ficar em uma biblioteca é bastante difícil, porque a escolha depende de muitos fatores: das preferências pessoais à ideologia do projeto.
Eu faria o seguinte:
- Se você está apenas começando o seu caminho no desenvolvimento, tente iniciar um pequeno projeto com o AdapterDelegates - esta é a biblioteca mais simples - não é necessário nenhum conhecimento especial. Você entenderá como trabalhar com ele e por que é mais conveniente do que escrever adaptadores.
- O Groupie é adequado para aqueles que já jogaram o suficiente com o AdapterDelegates e estão cansados de escrever um monte de clichê, ou para todos os outros que imediatamente querem começar com um meio termo. E não se esqueça da presença de grupos dobráveis fora da caixa - esse também é um bom argumento a seu favor.
- Bem, e Epóxi - para aqueles que se deparam com um projeto verdadeiramente complexo, com uma enorme quantidade de dados, portanto a complexidade de uma biblioteca gorda será menos problemática. No começo, será difícil, mas a implementação das listas parecerá um pouco. Um argumento importante a favor do Epoxy pode ser a presença de DataBinding e MVVM no projeto - ele foi literalmente criado para isso, dada a possibilidade de gerar modelos a partir dos layouts correspondentes.
Se você ainda tiver dúvidas, pode consultar o link para ver novamente o código do nosso aplicativo com os detalhes.