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.
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.
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.
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.
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.
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.
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:
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.
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.
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)
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 } } }
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.
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 } }
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 dadaHasDecorations
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.
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.