paint-brush
Listas vintage con superpoderes en Android: Delegación como basepor@6hundreds
330 lecturas
330 lecturas

Listas vintage con superpoderes en Android: Delegación como base

por Sergey Opivalov9m2023/07/01
Read on Terminal Reader

Demasiado Largo; Para Leer

Muchos proyectos todavía usan el marco View y, por lo tanto, usan RecyclerView para tener listas. Este artículo es el resultado de una recopilación de mi experiencia con RecylerView. Hablaré sobre mi visión para construir un marco alrededor de RecylderView.
featured image - Listas vintage con superpoderes en Android: Delegación como base
Sergey Opivalov HackerNoon profile picture
0-item
1-item

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 de Android . Allí se eliminó tanta creación de listas repetitivas. Y es hermoso


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 DiffUtil para actualizar una lista de datos. No vamos a reinventar una rueda aquí, así que nos centraremos en aprovechar DiffUtil y también intentaremos reducir una ceremonia para su implementación.


  • 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:


isItemTheSame verifica la identidad de other y this

isContentTheSame comprueba los datos en other y 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 } }

isContentTheSame en la gran mayoría de los casos será bastante simple (una verificación de igualdad), si las implementaciones de ListItem serán una data class .


Mantener ListItem 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:


 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 DiffUtil muy natural y única:

 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 AsyncListDifferDelegationAdapter desde AdapterDelegates:

 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 TitleListItem que contienen un campo de texto:

 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 RecyclerView , 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: all items should have offset at the bottom except the last one , o all items should have a divider at the bottom, but headers should have an offset at bottom , entonces crear una decoración se convierte en una molestia.


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 RecyclerView y delegar una implementación.


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 var en aras de la simplicidad para cambiarlas en tiempo de ejecución, si es necesario.


Muy a menudo, un elemento tiene una sola decoración, por lo que para reducir el modelo estándar envolviendo un solo elemento en listOf() podemos hacer tal maniobra:

 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 ItemDecoration con una sola diferencia: ahora es consciente del tipo de decoración y la instancia que se avecina.


Las posibles implementaciones para DecorationDrawer es un tema del segundo artículo, ahora centrémonos solo en la interfaz y cómo debe manejarse para presentar las decoraciones.

 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 } }
  1. Caché para cajones creados
  2. getAdapterPositionForView() 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
  3. Iterando a través de las decoraciones de la instancia de HasDecorations
  4. 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 RecyclerView . Delegación utilizada como principio básico para envolver elementos de lista y decoraciones de lista.


Creo que las principales ventajas de la solución sugerida son:

  1. 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.

  2. Escalabilidad. Gracias al principio de delegación y separación de preocupaciones, podemos desacoplar implementaciones de elementos delegados o cajones de decoración.

  3. 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.