Nowadays, Jetpack Compose gains an adoption and it definitely deserves it. It is a real paradigm shift in UI development. So much creating lists boilerplate was eliminated there. And it’s beautiful. Android I believe that many projects are still using the View framework and therefore using RecyclerView for having lists. There are a few reasons for it, as I see: Some in-house proprietary solutions around RecyclerView doesn’t match with Compose easily, so migration costs are high Developers are not familiar enough with Compose yet There is no confidence about performance of Compose (yet) But anyway, Compose is the future and this is why I’m using the word “vintage” in a title. Vintage means old but gold. This article is the result of a compilation of my experience with RecyclerView, and for those who are still on track with it, I’ll tell about my vision to building a framework around RecyclerView. Requirements The framework should provide a sane level of scalability. So the main scenarios should be handled with a minimum amount of boilerplate, but it shouldn’t get in the way if the user wants to create something custom. I would define the base set of scenarios during work with RecyclerView as next: Effective update of data. It is known best practice during working with RecyclerView is using API for updating a data in list. We are not going to re-invent a wheel here, so we’ll focus on leveraging and also will try to reduce a ceremony for its implementation. DiffUtil DiffUtil Defining heterogeneous lists. It is natural to have several types of views in a list. Screens of mobile devices are constrained by height and different devices has different screen sizes. So even your content is fitting to screen at one device without scrolling, it could require to be scrolled at another device. That’s why it can be a good idea to develop some screens on the top of RecyclerView. Decorating list items Decorations are a very common case for lists. And although you can have decorations merged to items layouts, what is can look straightforward, it’s not very flexible: same items types can require different decorations at different screens or even different decorations depending on item position in a list. Foundation options The first option is to make everything from scratch. It’s giving full control over implementation, but we can always do it. There are a bunch of third party open-source frameworks that allowing to work with RecyclerView with less boilerplate. I’m not going to enumerate all, but want to show two of them with completely opposite approaches: Epoxy Solution from AirBnb for creating complex screens with RecyclerView. It is adding a bunch of new APIs and even a code generation to reduce a boilerplate as much as possible. But I can't get rid of the feeling that resulting code is looking foreign. Also, an annotation processing adding another layer of complexity. AdapterDelegates Tiny library, that actually provides a concise bunch of functions and interfaces that only suggest you a way to split your heterogeneous list to set of delegated adapters. In fact, you can create such own solution in relatively short time. I like small native solutions that simple enough to keep all the behavior at fingertips and that doesn’t hide under carpet all the implementation details. That’s why I believe that AdapterDelagates and its principles are a good candidate for the foundation of our framework. List items The very basic thing for the beginning is a common declaration of list items. List items in the real world are very different, but the only thing we are confident in – we should be able to compare it. I suggest to referring to API of DiffUtil.ItemCallback interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } } It is a minimalistic (and therefore solid, IMO) declaration of a list item in the framework. Semantic of methods is intended to be the same as a DiffUtil using: checks identity of and isItemTheSame other this checks the data in and isContentTheSame other this The most common way to provide a stable and unique identity is using identifiers that came from the server. So let’s have an abstract implementation to reduce a bit of possible boilerplate: 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 } } in the vast majority of cases will be quite simple (an equality check), if implementations of ListItem will be a . isContentTheSame data class Keeping separate is still reasonable in cases when you have items in your list that are not projection of server data and haven't any sane identity. For example, if you have a footer and header as items in a list, or you have a single item of some type: ListItem data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem } Suggested approach allows us to have a very natural and single callback implementation: 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) } And the final step here is a declaration of default adapter that extends from AdapterDelegates: AsyncListDifferDelegationAdapter class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil) Heterogeneous lists AdapterDelegates library provides a handy way to declare delegates for a particular view type. Only thing we can improve is reduce a boilerplate a bit: 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) For showcase purposes, let’s consider an example how would declaration of some , containing one text field could look like: TitleListItem data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem() and delegate for it 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 } } } Decorated lists Originally, because of the API of , setting decorations are made separately from setting data to list. If you have some logic for decorations like: , or than creating decoration becomes a pain. 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 Often teams coming with “god” decorator that can be configured: 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() You can only imagine implementation details and how fragile and not scalable it is. We can use an old proven approach with mimicry to some API and delegating an implementation. RecyclerView So I’m starting with a data: interface Decoration Interface marker for decorations, that will be presented in the future. interface HasDecorations { var decorations: List<Decoration> } This interface should be implemented by our data items to declare that this particular item wants to be decorated with a given list of decorations. Decorations are in sake of simplicity to change it in runtime, if needed. var Very often an item has a single decoration, so to reduce boilerplate with wrapping a single item to we can do such maneuver: 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") } } } Next stage is mimic to 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 ) } As you can see, it completely repeats with a single difference – now it’s aware of the decoration type and instance is coming. ItemDecoration Possible implementations for is a topic of the second article, now let’s focus only on interface and how it should be handled to presenting decorations. 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 for created drawers is not something specific to decoration applying, it’s just a method for correctly resolving adapter position for given view getAdapterPositionForView() Iterating through decorations of instance HasDecorations This piece is a topic for second article “Decorations à la carte” Conclusion This article discovered an approach to build a list framework around . Delegation used as a base principle for wrapping list items and list decorations. RecyclerView I believe that the main advantages of the suggested solution is: Nativeness. Implementation is not depending on third party libraries, that can reduce flexibility in the future. AdapterDelegates was chosen to consider exactly these criteria – it’s very simple and natural and all the behavior at your fingertips. Scalability. Thanks to delegation and separation of concerns principle, we are able to decouple implementations of items delegates or decorations drawers. It’s easy to integrate incrementally to an existing project In the second part, we’ll discuss all types of list decorations that we probably expect to see in an Android application.