Dynamic RecyclerView.Adapter: Mastering Heterogeneous Lists in Android Apps
The Challenge: Displaying Diverse Data
When building Android apps, you often encounter the need to display a list of items with varying layouts and content. This heterogeneous list can include grids, nested horizontal lists, and more. To tackle this challenge, you need a dynamic RecyclerView.Adapter that can handle different ViewHolder types for backend-controlled viewTypes.
Structuring Backend Responses
Before diving into the code, let’s explore how backend responses are typically structured. A server response might look like this:
{ "type": "banner",... }
{ "type": "quote",... }
{ "type": "carousel",... }
Using a JSON deserializer like Gson or Jackson can lead to runtime crashes. One solution is to create a data class, but this approach becomes cumbersome as the number of types grows.
A More Elegant Approach: Moshi Polymorphic JSON Adapter
To streamline the process, you can use Moshi’s polymorphic JSON adapter with Retrofit. Start by adding the required dependencies to your build.gradle file:
dependencies { implementation 'com.squareup.moshi:moshi:1.12.0' }
Next, create Kotlin data classes for each type, using a sealed class and an enum to map the different types:
enum class Type { BANNER, QUOTE, CAROUSEL,... }
sealed class Data { abstract val type: Type }
data class BannerData(val type: Type = Type.BANNER,...) : Data()
data class QuoteData(val type: Type = Type.QUOTE,...) : Data()
...
Creating the UI Models and ViewHolders
For a clean approach, create separate data classes for your UI logic. Start with a marker interface:
interface UIModel
Then, create a utility extension to map each data model to the UI model:
fun Data.toUIModel(): UIModel
Create different UIs for each type, along with their corresponding ViewHolders:
BannerViewHolder
CarouselViewHolder
GridViewHolder
QuoteViewHolder
ContactsViewHolder
Each ViewHolder extends a base class:
abstract class BaseViewHolder<T : UIModel>(view: View) : RecyclerView.ViewHolder(view)
The Dynamic Adapter
Override the getItemViewType()
method to inform the RecyclerView.Adapter about the different viewTypes:
override fun getItemViewType(position: Int): Int = viewType.ordinal
In the onCreateViewHolder()
method, check the viewType field and create the appropriate ViewHolder:
when (viewType) { BANNER -> BannerViewHolder(...) }
Finally, in the onBindViewHolder()
method, call holder.bind(uiList[position])
on the BaseViewHolder.
Adhering to SOLID Principles
While the above code achieves the goal, it’s not perfect. To adhere to SOLID principles, move the ViewHolder instantiation code to each individual ViewHolder class. Then, use the Visitor pattern to delegate the responsibility of checking viewTypes to a separate class:
interface Visitable<T> { fun accept(visitor: Visitor<T>) }
class TypeFactory { fun createViewHolder(viewType: Int): RecyclerView.ViewHolder }
By using the Visitor pattern, you can keep your adapter class lean and avoid modifying it when adding new viewTypes.
Summary
In this article, you learned how to create a dynamic RecyclerView.Adapter that can handle heterogeneous lists with different ViewHolder types for backend-controlled viewTypes. You also saw how to use Moshi’s polymorphic JSON adapter to streamline the process. Finally, you discovered how to adhere to SOLID principles using the Visitor pattern.