paint-brush
Super-Powered Vintage Lists in Android: Delegation as a Foundationby@6hundreds
330 reads
330 reads

Super-Powered Vintage Lists in Android: Delegation as a Foundation

by Sergey OpivalovJuly 1st, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Many projects are still using the View framework and therefore using RecyclerView for having lists. This article is the result of a compilation of my experience with RecylerView. I’ll tell about my vision to building a framework around RecylderView.
featured image - Super-Powered Vintage Lists in Android: Delegation as a Foundation
Sergey Opivalov HackerNoon profile picture

Nowadays, Jetpack Compose gains an adoption and it definitely deserves it. It is a real paradigm shift in Android UI development. So much creating lists boilerplate was eliminated there. And it’s beautiful.


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 DiffUtil API for updating a data in list. We are not going to re-invent a wheel here, so we’ll focus on leveraging DiffUtil and also will try to reduce a ceremony for its implementation.


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


isItemTheSame checks identity of other and this

isContentTheSame checks the data in other and 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
 }
}

isContentTheSame in the vast majority of cases will be quite simple (an equality check), if implementations of ListItem will be a data class.


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


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 DiffUtil callback implementation:

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

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 TitleListItem, containing one text field could look like:

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 RecyclerView, setting decorations are made separately from setting data to list. If you have some logic for decorations like: all items should have offset at the bottom except the last one, or all items should have a divider at the bottom, but headers should have an offset at bottom than creating decoration becomes a pain.


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 someRecyclerView API and delegating an implementation.


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 var in sake of simplicity to change it in runtime, if needed.


Very often an item has a single decoration, so to reduce boilerplate with wrapping a single item to listOf() we can do such maneuver:

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 ItemDecoration API:

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 ItemDecoration with a single difference – now it’s aware of the decoration type and instance is coming.


Possible implementations for DecorationDrawer is a topic of the second article, now let’s focus only on interface and how it should be handled to presenting decorations.

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. Cache for created drawers
  2. getAdapterPositionForView() is not something specific to decoration applying, it’s just a method for correctly resolving adapter position for given view
  3. Iterating through decorations of HasDecorations instance
  4. This piece is a topic for second article “Decorations à la carte”

Conclusion

This article discovered an approach to build a list framework around RecyclerView. Delegation used as a base principle for wrapping list items and list decorations.


I believe that the main advantages of the suggested solution is:

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

  2. Scalability. Thanks to delegation and separation of concerns principle, we are able to decouple implementations of items delegates or decorations drawers.

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