
Ilya Nekrasov, Mahtalitet , desarrollador de Android de KODE
Durante dos años y medio en el desarrollo de Android, logré trabajar en proyectos completamente diferentes: desde una red social para automovilistas y un banco letón hasta un sistema de bonificación federal y la tercera aerolínea para el transporte. De todos modos, en cada uno de estos proyectos, me encontré con tareas que requerían la búsqueda de soluciones no clásicas al implementar listas utilizando la clase RecyclerView.
Este artículo, fruto de la preparación para una presentación en DevFest Kaliningrad'18, así como de comunicarse con colegas, será especialmente útil para los desarrolladores principiantes y aquellos que han usado solo una de las bibliotecas existentes.
Para comenzar, profundicemos un poco en la esencia del problema y la fuente del dolor, es decir, el crecimiento de la funcionalidad en el desarrollo de la aplicación y la complejidad de las listas utilizadas.
Capítulo uno, en el que el cliente sueña con una aplicación, y nosotros, sobre requisitos claros
Imaginemos una situación en la que un cliente contacta a un estudio que quiere una aplicación móvil para una tienda de patos de goma.
El proyecto se está desarrollando rápidamente, surgen nuevas ideas regularmente y no se enmarcan en una hoja de ruta a largo plazo.
Primero, el cliente nos pide que muestremos una lista de productos existentes y, cuando se hace clic en ella, completamos una solicitud de entrega. No tiene que ir muy lejos para encontrar una solución: utilizamos el conjunto clásico de RecyclerView , un adaptador simple escrito para él y Activity .
Para el adaptador, utilizamos datos homogéneos, un ViewHolder y una lógica de enlace simple.
Adaptador con 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) } }
Con el tiempo, el cliente tiene la idea de agregar otra categoría de productos a los patos de goma, lo que significa que tendrá que agregar un nuevo modelo de datos y un nuevo diseño. Pero lo más importante, aparecerá otro ViewType en el adaptador, con el que puede determinar qué ViewHolder usar para un elemento de lista específico.
Después de eso, los encabezados se agregan a las categorías, según las cuales cada categoría se puede contraer y expandir para simplificar la orientación de los usuarios en la tienda. Esto es más otro ViewType y ViewHolder para encabezados. Además, tendrá que complicar el adaptador, ya que necesita mantener una lista de grupos abiertos y usarla para verificar la necesidad de ocultar y mostrar este o aquel elemento haciendo clic en el encabezado.
Adaptador con todo 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) }
Creo que entiendes el punto: tal montón de pequeños se asemeja a un desarrollo saludable. Además, cada vez hay más requisitos nuevos del cliente: fijar un banner publicitario en la parte superior de la lista, darse cuenta de la posibilidad de elegir el número de patos ordenados. Solo estas tareas finalmente se convertirán en adaptadores regulares, que nuevamente tendrán que escribirse desde cero.
El proceso de desarrollar un adaptador clásico en la historia de github
Resumen
De hecho, la imagen no es nada alentadora: los adaptadores individuales deben ser afilados para casos específicos. Todos entendemos que en una aplicación real hay docenas o incluso cientos de tales pantallas de lista. Y no contienen información sobre patos, sino datos más complejos. Sí, y su diseño es mucho más complicado.
¿Qué hay de malo con nuestros adaptadores?
- obviamente, son difíciles de reutilizar;
- dentro de la lógica de negocios aparece y con el tiempo se vuelve más y más;
- difícil de mantener y expandir;
- alto riesgo de errores al actualizar datos;
- Diseño no obvio.
Capítulo dos, en el que todo podría ser diferente
No es realista ni tiene sentido imaginar el desarrollo de la aplicación en los años venideros. Después de un par de bailes con una pandereta como en el último capítulo y escribir docenas de adaptadores, cualquiera tendrá una pregunta: "¿Quizás hay otras soluciones?".

Después de completar Github, encontramos que la primera biblioteca AdapterDelegates apareció en 2015, y un año después, Groupie y Epoxy se agregaron al arsenal de desarrolladores: todos ayudan a facilitar la vida, pero cada uno tiene sus propios detalles y dificultades.
Hay varias bibliotecas más similares (por ejemplo, FastAdapter), pero ni yo ni mis colegas trabajamos con ellas, por lo que no las consideraremos en el artículo.
Antes de comparar las bibliotecas, analizamos brevemente el caso descrito anteriormente con la tienda en línea con la condición de usar AdapterDelegates : de las bibliotecas desmontadas, es la más simple desde el punto de vista de la implementación y el uso interno (sin embargo, no está avanzado en todo, por lo que debe agregar muchas cosas a mano).
La biblioteca no nos salvará completamente del adaptador, pero se formará a partir de bloques (ladrillos), que podemos agregar o quitar de la lista de forma segura e intercambiarlos.
Adaptador de bloque 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) } }
Usar un adaptador de actividad 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 la primera tarea, vemos la diferencia: tenemos una clase de adaptador que hereda de la biblioteca. Y además, el mismo ladrillo, que se llama delegado y del que también heredamos e implementamos parte de la lógica que necesitamos. A continuación, agregamos un delegado al administrador ; esta también es una clase de biblioteca. Y lo último que necesita es crear un adaptador y llenarlo con datos
Para implementar la segunda categoría de la tienda y los encabezados, escribiremos un par de delegados más y la animación aparecerá gracias a la clase DiffUtil .
Aquí indicaré una conclusión breve pero definitiva: el uso de incluso esta biblioteca resuelve todos los problemas enumerados que surgieron cuando complicamos la aplicación en un caso con una tienda en línea, pero sin los inconvenientes, en ningún otro lado.
Proceso de desarrollo del adaptador con AdapterDelegates en el historial de github
Capítulo tres, en el que el desarrollador elimina las gafas de color rosa al comparar bibliotecas
Nos sumergiremos en más detalle en la funcionalidad y operación de cada una de las bibliotecas. De una forma u otra, utilicé las tres bibliotecas en nuestros proyectos, dependiendo de las tareas y la complejidad de la aplicación.
Utilizamos esta biblioteca en la aplicación de una de las mayores aerolíneas rusas. Necesitábamos reemplazar una lista de pago simple con una lista con grupos y una gran cantidad de parámetros diferentes.
Un esquema de trabajo de biblioteca simplificado se ve así: 
La clase principal es el DelegateAdapter , los diversos "ladrillos" son los "delegados" responsables de mostrar un tipo de datos en particular y, por supuesto, la lista misma.
Pros:
- simplicidad de inmersión;
- adaptadores fáciles de reutilizar;
- pocos métodos y clases;
- sin reflexión, generación de código y enlace de datos.
Contras:
- necesita implementar la lógica usted mismo, por ejemplo
actualizar elementos a través de DiffUti l (desde la versión 3.1.0 puede usar el adaptador AsyncListDifferDeferationAdapter); - código redundante
En general, esta biblioteca resuelve todas las dificultades principales para expandir la funcionalidad de la aplicación y es adecuada para aquellos que no han usado la biblioteca antes. Pero para detenerse solo en eso, no lo aconsejo.
A menudo usamos Groupie , creado hace varios años por Lisa Wray , que incluye escribir la solicitud para un banco letón que lo usa por completo.
Para utilizar esta biblioteca, en primer lugar, debe lidiar con las dependencias . Además de la principal, puede usar varias opciones para elegir:
Nos detenemos en una cosa y prescribimos las dependencias necesarias.
Usando un ejemplo de una tienda en línea con patos, necesitamos crear un artículo heredado de la clase de biblioteca, especificar el diseño e implementar el enlace a través de los sintéticos de Kotlin. En comparación con la cantidad de código que tuve que escribir con AdapterDelegates , es solo el cielo y la tierra.
Todo lo que queda es configurar el RecyclerView GroupieAdapter como un adaptador y colocar los elementos simulados en él.

Se puede ver que el esquema de trabajo es más grande y más complejo. Aquí, además de elementos simples, puede usar secciones enteras: grupos de elementos y otras clases.
Pros:
- interfaz intuitiva, aunque api te hace pensar;
- la presencia de soluciones en caja;
- desglose en grupos de elementos;
- la elección entre la opción habitual, Kotlin Extensions y DataBinding;
- incrustación de ItemDecoration y animaciones.
Contras:
- wiki incompleto;
- poco apoyo por parte del mantenedor y la comunidad;
- pequeños errores que tuvieron que ser evitados en la primera versión;
- que difiere en el hilo principal (por ahora);
- no hay soporte para AndroidX (por el momento, pero necesita monitorear el repositorio)
Es importante que Groupie, con todos sus inconvenientes, pueda reemplazar fácilmente AdapterDelegates , especialmente si planea hacer listas plegables del primer nivel, y no quiere escribir mucho repetitivo.
Implementando la lista de patos con Groupie
La última biblioteca que comenzamos a usar relativamente recientemente es Epoxy , desarrollada por los chicos de Airbnb . La biblioteca es compleja, pero le permite resolver una amplia gama de tareas. Los propios programadores de Airbnb lo usan para representar pantallas directamente desde el servidor. Epoxy fue útil en uno de nuestros últimos proyectos: una solicitud para un banco en Ekaterimburgo.
Para desarrollar pantallas, tuvimos que trabajar con diferentes tipos de datos, una gran cantidad de listas. Y una de las pantallas era realmente interminable. Y Epoxy nos ayudó a todos a lidiar con esto.
El principio de la biblioteca en su conjunto es similar a los dos anteriores, excepto que en lugar de un adaptador, EpoxyController se usa para crear la lista, lo que le permite determinar declarativamente la estructura del adaptador.

Para lograr esto, la biblioteca se basa en la generación de código. Cómo funciona: con todos los matices, está bien descrito en la wiki y se refleja en las muestras .
Pros:
- modelos para la lista, generados a partir de la Vista habitual, con posibilidad de reutilización en pantallas simples;
- descripción declarativa de pantallas;
- Enlace de datos a la velocidad máxima: genera modelos directamente a partir de archivos de diseño;
- simplemente muestre no solo listas de los bloques, sino también pantallas complejas;
- ViewPool compartido en Actividad;
- diferenciación asincrónica fuera de la caja (AsyncEpoxyController);
- No es necesario vaporizar con listas horizontales.
Contras:
- gran cantidad de clases, procesadores, anotaciones;
- inmersión difícil desde cero;
- usa el complemento ButterKnife para generar archivos R2 en módulos;
- es muy difícil entender cómo trabajar correctamente con Callbacks (nosotros mismos aún no lo entendíamos);
- Hay problemas que deben evitarse: por ejemplo, un bloqueo con la misma identificación.
Implementando la Lista de Patos con Epoxy
Resumen
Lo principal que quería transmitir era: no tolerar la complejidad que aparece cuando necesitas hacer listas complejas y constantemente tienes que rehacerlas. Y esto sucede muy a menudo. Y, en principio, cuando se implementan, si el proyecto recién comienza o si está refactorizando.
La realidad es que no es necesario complicar la lógica una vez más, pensando que bastará algún tipo de abstracción propia. Son lo suficientemente cortos y trabajar con ellos no solo no es un placer, sino que también existe la tentación de transferir parte de la lógica a la parte de la interfaz de usuario, que no debería estar allí. Existen herramientas que pueden ayudarlo a evitar la mayoría de los problemas, y debe usarlas.
Entiendo que para muchos desarrolladores experimentados (y no solo) esto es obvio o pueden estar en desacuerdo conmigo. Pero considero importante enfatizar esto nuevamente.
Entonces, ¿qué elegir?
Aconsejar permanecer en una biblioteca es bastante difícil, porque la elección depende de muchos factores: desde las preferencias personales hasta la ideología del proyecto.
Yo haría lo siguiente:
- Si recién está comenzando su camino en el desarrollo, intente comenzar en un pequeño proyecto con AdapterDelegates , esta es la biblioteca más simple, no se requieren conocimientos especiales. Comprenderá cómo trabajar con él y por qué es más conveniente que escribir adaptadores usted mismo.
- Groupie es adecuado para aquellos que ya han jugado lo suficiente con AdapterDelegates y están cansados de escribir un montón de repeticiones, o para todos los que inmediatamente quieren comenzar con un término medio. Y no se olvide de la presencia de grupos plegables fuera de la caja ; este también es un buen argumento a su favor.
- Bueno, y Epoxy : para aquellos que se enfrentan a un proyecto verdaderamente complejo, con una gran cantidad de datos, por lo que la complejidad con una gran biblioteca será un problema menor. Al principio será difícil, pero una mayor implementación de las listas parecerá un poco insignificante. Un argumento importante a favor de Epoxy puede ser la presencia de DataBinding y MVVM en el proyecto: se creó literalmente para esto, dada la posibilidad de generar modelos a partir de los diseños correspondientes.
Si aún tiene preguntas, puede mirar el enlace para ver nuevamente el código de nuestra aplicación con los detalles.