Kotlin’s sealed class ordinals with Recyclerview adapter
Let’s perfect RecyclerView’s adapter item types with sealed classes.
Here’s a small “code bliss” pattern which we use in Nutech — Android jobs app’s list adapters.
With this code pattern, we believe we finally have one, clean way to write adapters.
Let’s start.
By now, we assume (and hope) that you already use sealed classes for both Adapters’ items and view holders. So your average case should look pretty much like this:
And in this scenario, because we have more than one item type, we have to define the item type in the adapter. The type must be defined with an Int. So we usually do something like:
class JobsListAdapter : ListAdapter<JobListItem, JobViewHolder>() {
override fun getItemViewType(position: Int): Int {
// several ways to do that. One of them would be:
return when (getItem(position)) {
is Header -> 0 <-- let's simplify this
is JobItem -> 1 <-- let's simplify this
}
}
override fun onCreateViewHolder(...): JobViewHolder {
return when (viewType) {
0 -> HeaderViewHolder(...) <-- let's simplify this
1 -> JobViewHolder(...). <-- let's simplify this
else -> throw IllegalStateException()
}
}
}
Let’s change the above and automate the above types (0 and 1 above) with ordinals! As if a sealed class was an enum. To do that, we need to:
- Return the type in the getItemViewType
- Match the type of the onCreateViewHolder
The trick here? Make our sealed class items have ordinals. Like enums do.
Calculate the type of the instance
The first step to automate this type’s calculation is to calculate the subclass’s ordinal
inline fun <reified T : Any> T.ordinal(): Int =
T::class.java.classes.indexOfFirst { sub -> sub == javaClass }
That way you can now get the type of any item in one function in a generic way:
class JobsListAdapter : ListAdapter<JobListItem, JobViewHolder>() {
override fun getItemViewType(position: Int): Int {
return getItem(position).ordinal()
}
}
So i.e. a HeaderViewHolder item will return 0 and JobViewHolder item will return 1.
Calculate the type of the class, statically
The second (and final step) is that we wish to use the .ordinal() statically like:
override fun onCreateViewHolder(...): JobViewHolder {
return when (viewType) {
Header.ordinal() -> HeaderViewHolder(...)
JobItem.ordinal() -> JobViewHolder(...)
else -> throw IllegalStateException()
}
}
Thus, let's extend the .ordinal() function for the static usage:
inline fun <reified T : Any> T.ordinal(): Int {
// we differenciate if it's top level sealed class here 👇
if (T::class.isSealed) {
return T::class.java.classes.indexOfFirst { sub -> sub == javaClass }
} // and we now added this part 👇
val klass = if (T::class.isCompanion) {
javaClass.declaringClass
} else {
javaClass
}
return klass.superclass?.classes?.indexOfFirst { it == klass } ?: -1
}
🔥 Issue, this will not work for data classes, only for objects. So in our example, JobItem.ordinal() cannot be called because JobItem is a data class.
The solution? ✅ An empty companion object in each data class:
sealed class JobListItem {
val type: Int = type()
object Header : JobListItem()
data class JobItem(val payload: Payload) : JobListItem(){
companion object <-- with this JobItem.ordinal() now works
}
}
Final result
Getting and resolving the view holder’s type is now just a readability bliss 💯
⚠️ Note: The code ordinal() will work only with 1st level sealed classes. If you need something more sophisticated, make sure to adjust that method accordingly.
⚠️Note 2: Like many features using reflection, when using R8’s or Proguard’s obfuscation, you can encounter runtime crashes. You need to make sure both the classes and the companion objects are available during runtime. One way to do it is with the @Keep
annotation.
Thank you for reading!
Want to learn more about me? I’m Dionysis, an Android dev at ShareNow + Founder of Nutech, a jobs app only for Android developers.
If you are an Android developer, you will most likely be interested in Nutech app.