Hoy en día, Jetpack Compose gana una adopción y definitivamente lo merece. Es un cambio de paradigma real en el desarrollo de la interfaz de usuario . Allí se eliminó tanta creación de listas repetitivas. Y es hermoso de Android Creo que muchos proyectos todavía usan View framework y, por lo tanto, usan RecyclerView para tener listas. Hay algunas razones para ello, como veo: Algunas soluciones propietarias internas en torno a RecyclerView no coinciden fácilmente con Compose, por lo que los costos de migración son altos. Los desarrolladores aún no están lo suficientemente familiarizados con Compose No hay confianza sobre el rendimiento de Compose (todavía) Pero de todos modos, Compose es el futuro y es por eso que estoy usando la palabra "vintage" en un título. Vintage significa viejo pero dorado. Este artículo es el resultado de una compilación de mi experiencia con RecyclerView, y para aquellos que todavía están al tanto, les contaré mi visión para construir un marco alrededor de RecyclerView. Requisitos El marco debe proporcionar un nivel razonable de escalabilidad. Por lo tanto, los escenarios principales deben manejarse con una cantidad mínima de repeticiones, pero no deben interferir si el usuario desea crear algo personalizado. Definiría el conjunto base de escenarios durante el trabajo con RecyclerView de la siguiente manera: Actualización efectiva de datos. Se sabe que la mejor práctica durante el trabajo con RecyclerView es usar la API para actualizar una lista de datos. No vamos a reinventar una rueda aquí, así que nos centraremos en aprovechar y también intentaremos reducir una ceremonia para su implementación. DiffUtil DiffUtil Definición de listas heterogéneas. Es natural tener varios tipos de vistas en una lista. Las pantallas de los dispositivos móviles están limitadas por la altura y los diferentes dispositivos tienen diferentes tamaños de pantalla. Entonces, incluso su contenido se ajusta a la pantalla en un dispositivo sin desplazarse, podría requerir que se desplace en otro dispositivo. Es por eso que puede ser una buena idea desarrollar algunas pantallas en la parte superior de RecyclerView. Elementos de la lista de decoración Las decoraciones son un caso muy común para las listas. Y aunque puede tener decoraciones fusionadas con diseños de elementos, lo que puede parecer sencillo, no es muy flexible: los mismos tipos de elementos pueden requerir diferentes decoraciones en diferentes pantallas o incluso diferentes decoraciones dependiendo de la posición del elemento en una lista. Opciones de cimentación La primera opción es hacer todo desde cero. Da control total sobre la implementación, pero siempre podemos hacerlo. Hay un montón de marcos de código abierto de terceros que permiten trabajar con RecyclerView con menos repeticiones. No voy a enumerar todos, pero quiero mostrar dos de ellos con enfoques completamente opuestos: Epoxy Solución de AirBnb para crear pantallas complejas con RecyclerView. Está agregando un montón de nuevas API e incluso una generación de código para reducir un modelo tanto como sea posible. Pero no puedo deshacerme de la sensación de que el código resultante parece extraño. Además, un procesamiento de anotaciones agrega otra capa de complejidad. Delegados del adaptador Pequeña biblioteca, que en realidad proporciona un conjunto conciso de funciones e interfaces que solo le sugieren una forma de dividir su lista heterogénea en un conjunto de adaptadores delegados. De hecho, puede crear una solución propia en un tiempo relativamente corto. Me gustan las soluciones nativas pequeñas que son lo suficientemente simples como para mantener todo el comportamiento al alcance de la mano y que no ocultan debajo de la alfombra todos los detalles de implementación. Por eso creo que AdapterDelagates y sus principios son un buen candidato para la base de nuestro marco. Elementos de la lista Lo más básico para el comienzo es una declaración común de los elementos de la lista. Los elementos de la lista en el mundo real son muy diferentes, pero lo único en lo que confiamos es que deberíamos poder compararlo. Sugiero referirme a la API de DiffUtil.ItemCallback interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } } Es una declaración minimalista (y por lo tanto sólida, en mi opinión) de un elemento de lista en el marco. Se pretende que la semántica de los métodos sea la misma que un DiffUtil usando: verifica la identidad de y isItemTheSame other this comprueba los datos en y isContentTheSame other this La forma más común de proporcionar una identidad única y estable es usar identificadores que provienen del servidor. Entonces, tengamos una implementación abstracta para reducir un poco el posible texto repetitivo: abstract class DefaultListItem : ListItem { abstract val id: String override fun isItemTheSame(other: ListItem): Boolean = when { other !is DefaultListItem -> false this::class != other::class -> false else -> this.id == other.id } } en la gran mayoría de los casos será bastante simple (una verificación de igualdad), si las implementaciones de ListItem serán una . isContentTheSame data class Mantener separado sigue siendo razonable en los casos en que tiene elementos en su lista que no son una proyección de los datos del servidor y no tienen una identidad sana. Por ejemplo, si tiene un pie de página y un encabezado como elementos en una lista, o si tiene un único elemento de algún tipo: ListItem data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem } El enfoque sugerido nos permite tener una implementación de devolución de llamada muy natural y única: DiffUtil object DefaultDiffUtil : DiffUtil.ItemCallback<ListItem>() { override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isItemTheSame(newItem) override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isContentTheSame(newItem) } Y el paso final aquí es una declaración del adaptador predeterminado que extiende desde AdapterDelegates: AsyncListDifferDelegationAdapter class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil) Listas heterogéneas La biblioteca AdapterDelegates proporciona una forma práctica de declarar delegados para un tipo de vista en particular. Lo único que podemos mejorar es reducir un poco el texto modelo: inline fun <reified I : ListItem, V : ViewBinding> defaultAdapterDelegate( noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, noinline block: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit ) = adapterDelegateViewBinding<I, ListItem, V>(viewBinding = viewBinding, block = block) Para fines de presentación, consideremos un ejemplo de cómo se vería la declaración de algunos que contienen un campo de texto: TitleListItem data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem() y delegar para ello fun titleItemDelegate(onClick: ((String) -> Unit)) = defaultAdapterDelegate< TitleListItem, TitleListItemBinding >(viewBinding = { inflater, root -> TitleListItemBinding.inflate(inflater,root,false) }) { itemView.setOnClickListener { it(item.id) } bind { binding.root.text = item.title } } } Listas decoradas Originalmente, debido a la API de , las decoraciones de configuración se realizan por separado de la configuración de datos en la lista. Si tiene alguna lógica para decoraciones como: , o , entonces crear una decoración se convierte en una molestia. RecyclerView all items should have offset at the bottom except the last one all items should have a divider at the bottom, but headers should have an offset at bottom A menudo, los equipos vienen con un decorador "dios" que se puede configurar: class SmartDividerItemDecorator( val context: Context, val skipDividerFor: Set<Int> = emptySet(), val showDividerAfterLastItem: Boolean = false, val showDividerBeforeFirstItem: Boolean = false, val dividerClipToPadding: Boolean = true, val dividerPaddingLeft: Int = 0, val dividerPaddingRight: Int = 0 ) : ItemDecoration() Solo puede imaginar los detalles de implementación y cuán frágil y no escalable es. Podemos usar un antiguo enfoque probado con imitación de alguna API y delegar una implementación. RecyclerView Así que estoy empezando con un dato: interface Decoration Marcador de interfaz para decoraciones, que se presentará en el futuro. interface HasDecorations { var decorations: List<Decoration> } Esta interfaz debe ser implementada por nuestros elementos de datos para declarar que este elemento en particular quiere ser decorado con una lista dada de decoraciones. Las decoraciones son en aras de la simplicidad para cambiarlas en tiempo de ejecución, si es necesario. var Muy a menudo, un elemento tiene una sola decoración, por lo que para reducir el modelo estándar envolviendo un solo elemento en podemos hacer tal maniobra: listOf() interface HasDecoration : HasDecorations { var decoration: Decoration override var decorations: List<Decoration> get() = listOf(decoration) set(value) { decoration = when { value.isEmpty() -> None value.size == 1 -> value.first() else -> throw IllegalArgumentException("Applying few decorations to HasDecoration instance is prohibited. Use HasDecorations") } } } La siguiente etapa es imitar a la API : ItemDecoration interface DecorationDrawer<T : Decoration> { fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) fun onDraw( c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) } Como puede ver, repite completamente con una sola diferencia: ahora es consciente del tipo de decoración y la instancia que se avecina. ItemDecoration Las posibles implementaciones para es un tema del segundo artículo, ahora centrémonos solo en la interfaz y cómo debe manejarse para presentar las decoraciones. DecorationDrawer class CompositeItemDecoration( private val context: Context ) : RecyclerView.ItemDecoration() { private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // [1] private fun applyDecorationToView(parent: RecyclerView, view: View, applyDecoration: (Decoration) -> Unit) { val position = getAdapterPositionForView(parent, view) // [2] when { // [3] position == RecyclerView.NO_POSITION -> return adapter.items[position] is HasDecorations -> { val decoration = (adapter.items[position] as HasDecorations).decorations decoration.forEach(applyDecoration) } else -> return } } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { applyDecorationToView(parent, view) { decoration -> val drawer = getDrawerFor(decoration) drawer.getItemOffsets(outRect, view, parent, state, decoration) } } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.forEach { child -> applyDecorationToView(parent, child) { decoration -> val drawer = getDrawerFor(decoration) drawer.onDraw(c, child, parent, state, decoration) } } } private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> { // [4] } private fun getAdapterPositionForView(parent: RecyclerView, view: View): Int { var position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) { val oldPosition = parent.getChildViewHolder(view).oldPosition if (oldPosition in 0 until (parent.adapter?.itemCount ?: 0)) { position = oldPosition } } return position } } Caché para cajones creados no es algo específico para la aplicación de decoración, es solo un método para resolver correctamente la posición del adaptador para una vista dada getAdapterPositionForView() Iterando a través de las decoraciones de la instancia de HasDecorations Esta pieza es tema del segundo artículo “Decoraciones a la carta” Conclusión Este artículo descubrió un enfoque para crear un marco de lista en torno a . Delegación utilizada como principio básico para envolver elementos de lista y decoraciones de lista. RecyclerView Creo que las principales ventajas de la solución sugerida son: Natividad. La implementación no depende de bibliotecas de terceros, lo que puede reducir la flexibilidad en el futuro. AdapterDelegates fue elegido para considerar exactamente estos criterios: es muy simple y natural y todo el comportamiento está al alcance de su mano. Escalabilidad. Gracias al principio de delegación y separación de preocupaciones, podemos desacoplar implementaciones de elementos delegados o cajones de decoración. Es fácil de integrar de forma incremental a un proyecto existente En la segunda parte, discutiremos todos los tipos de decoraciones de listas que probablemente esperamos ver en una aplicación de Android.