De nos jours, Jetpack Compose est adopté et il le mérite vraiment. Il s'agit d'un véritable changement de paradigme dans le développement de l'interface utilisateur . Tant de listes de création passe-partout y ont été éliminées. Et c'est beau. Android Je crois que de nombreux projets utilisent encore le framework View et utilisent donc RecyclerView pour avoir des listes. Il y a plusieurs raisons à cela, comme je le vois: Certaines solutions propriétaires internes autour de RecyclerView ne correspondent pas facilement à Compose, de sorte que les coûts de migration sont élevés Les développeurs ne sont pas encore assez familiarisés avec Compose Il n'y a pas (encore) confiance dans les performances de Compose Mais quoi qu'il en soit, Compose est l'avenir et c'est pourquoi j'utilise le mot « vintage » dans un titre. Vintage signifie vieux mais doré. Cet article est le résultat d'une compilation de mon expérience avec RecyclerView, et pour ceux qui sont toujours sur la bonne voie, je parlerai de ma vision de la construction d'un cadre autour de RecyclerView. Exigences Le cadre devrait fournir un niveau sain d'évolutivité. Ainsi, les scénarios principaux doivent être gérés avec un minimum de passe-partout, mais cela ne doit pas gêner si l'utilisateur souhaite créer quelque chose de personnalisé. Je définirais l'ensemble de scénarios de base pendant le travail avec RecyclerView comme suit : Mise à jour efficace des données. Il est connu que la meilleure pratique lors de l'utilisation de RecyclerView consiste à utiliser l'API pour mettre à jour une donnée dans la liste. Nous n'allons pas réinventer une roue ici, nous nous concentrerons donc sur l'exploitation et essaierons également de réduire une cérémonie pour sa mise en œuvre. DiffUtil DiffUtil Définir des listes hétérogènes. Il est naturel d'avoir plusieurs types de vues dans une liste. Les écrans des appareils mobiles sont limités par la hauteur et différents appareils ont des tailles d'écran différentes. Ainsi, même votre contenu s'adapte à l'écran sur un appareil sans défilement, il peut être nécessaire de le faire défiler sur un autre appareil. C'est pourquoi il peut être judicieux de développer des écrans en haut de RecyclerView. Décorer les éléments de la liste Les décorations sont un cas très courant pour les listes. Et bien que vous puissiez fusionner des décorations avec des dispositions d'éléments, ce qui peut sembler simple n'est pas très flexible : les mêmes types d'éléments peuvent nécessiter des décorations différentes sur différents écrans ou même des décorations différentes en fonction de la position de l'élément dans une liste. Options de fondation La première option est de tout faire à partir de zéro. Cela donne un contrôle total sur la mise en œuvre, mais nous pouvons toujours le faire. Il existe un tas de frameworks open source tiers qui permettent de travailler avec RecyclerView avec moins de passe-partout. Je ne vais pas tous les énumérer, mais je veux en montrer deux avec des approches complètement opposées : Époxy Solution d'AirBnb pour créer des écrans complexes avec RecyclerView. Il ajoute un tas de nouvelles API et même une génération de code pour réduire autant que possible un passe-partout. Mais je ne peux pas me débarrasser du sentiment que le code résultant semble étranger. Aussi, un traitement d'annotation ajoutant une autre couche de complexité. AdaptateurDélégués Petite bibliothèque, qui fournit en fait un ensemble concis de fonctions et d'interfaces qui ne vous suggèrent qu'un moyen de diviser votre liste hétérogène en un ensemble d'adaptateurs délégués. En fait, vous pouvez créer une telle solution propre en un temps relativement court. J'aime les petites solutions natives suffisamment simples pour garder tous les comportements à portée de main et qui ne cachent pas sous le tapis tous les détails de mise en œuvre. C'est pourquoi je crois que AdapterDelagates et ses principes sont un bon candidat pour la fondation de notre framework. Éléments de la liste La chose la plus basique pour le début est une déclaration commune des éléments de la liste. Les éléments de liste dans le monde réel sont très différents, mais la seule chose en laquelle nous sommes confiants - nous devrions pouvoir les comparer. Je suggère de se référer à l'API de DiffUtil.ItemCallback interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } } Il s'agit d'une déclaration minimaliste (et donc solide, IMO) d'un élément de liste dans le framework. La sémantique des méthodes est censée être la même que celle d'un DiffUtil utilisant : vérifie l'identité des et isItemTheSame other this vérifie les données dans et isContentTheSame other this Le moyen le plus courant de fournir une identité stable et unique consiste à utiliser des identifiants provenant du serveur. Prenons donc une implémentation abstraite pour réduire un peu le passe-partout possible : 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 } } dans la grande majorité des cas sera assez simple (un contrôle d'égalité), si les implémentations de ListItem seront une . isContentTheSame data class Garder séparé est toujours raisonnable dans les cas où vous avez des éléments dans votre liste qui ne sont pas des projections de données de serveur et qui n'ont aucune identité saine. Par exemple, si vous avez un pied de page et un en-tête comme éléments dans une liste, ou si vous avez un seul élément d'un certain type : ListItem data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem } L'approche suggérée nous permet d'avoir une implémentation de rappel très naturelle et unique : 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) } Et la dernière étape ici est une déclaration d'adaptateur par défaut qui étend de AdapterDelegates : AsyncListDifferDelegationAdapter class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil) Listes hétérogènes La bibliothèque AdapterDelegates fournit un moyen pratique de déclarer des délégués pour un type de vue particulier. La seule chose que nous pouvons améliorer est de réduire un peu un passe-partout : 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) À des fins de démonstration, considérons un exemple à quoi ressemblerait la déclaration de certains , contenant un champ de texte : TitleListItem data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem() et déléguer pour cela 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 } } } Listes décorées À l'origine, à cause de l'API de , les décorations de paramètres sont faites séparément des données de paramètres à lister. Si vous avez une logique pour les décorations comme : , ou que la création de décoration devient une douleur. 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 Souvent, les équipes viennent avec un décorateur "dieu" qui peut être configuré : 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() Vous ne pouvez qu'imaginer les détails de mise en œuvre et à quel point il est fragile et non évolutif. Nous pouvons utiliser une ancienne approche éprouvée avec mimétisme à certaines API et déléguer une implémentation. RecyclerView Je commence donc avec une donnée : interface Decoration Marqueur d'interface pour les décorations, qui sera présenté dans le futur. interface HasDecorations { var decorations: List<Decoration> } Cette interface doit être implémentée par nos éléments de données pour déclarer que cet élément particulier souhaite être décoré avec une liste de décorations donnée. Les décorations sont dans un souci de simplicité pour les changer en cours d'exécution, si nécessaire. var Très souvent, un élément a une seule décoration, donc pour réduire le passe-partout en enveloppant un seul élément dans nous pouvons effectuer une telle manœuvre : 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 prochaine étape est l'imitation de l'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 ) } Comme vous pouvez le voir, il répète complètement avec une seule différence - maintenant il est conscient du type de décoration et de l'instance qui arrive. ItemDecoration Les implémentations possibles pour sont un sujet du deuxième article, concentrons-nous maintenant uniquement sur l'interface et sur la manière dont elle doit être gérée pour présenter les décorations. 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 } } Cache pour les tiroirs créés n'est pas quelque chose de spécifique à l'application de la décoration, c'est juste une méthode pour résoudre correctement la position de l'adaptateur pour une vue donnée getAdapterPositionForView() Itérer à travers les décorations de l'instance HasDecorations Cette pièce fait l'objet d'un second article « Décorations à la carte » Conclusion Cet article a découvert une approche pour créer un framework de liste autour de . Délégation utilisée comme principe de base pour envelopper les éléments de liste et les décorations de liste. RecyclerView Je crois que les principaux avantages de la solution suggérée sont : La naïveté. La mise en œuvre ne dépend pas de bibliothèques tierces, ce qui peut réduire la flexibilité à l'avenir. AdapterDelegates a été choisi pour tenir compte exactement de ces critères - c'est très simple et naturel et tous les comportements sont à portée de main. Évolutivité. Grâce au principe de délégation et de séparation des préoccupations, nous sommes en mesure de découpler les implantations d'éléments délégués ou de tiroirs de décors. Il est facile de s'intégrer progressivement à un projet existant Dans la deuxième partie, nous aborderons tous les types de décorations de liste que nous nous attendons probablement à voir dans une application Android.