RecyclerViews made easy with Epoxy

If you’re an Android developer, chances are at some point you had to make a RecyclerView with more than one view type. As the view types increase the complexity and boiler plate of writing a RecyclerView.Adapter increases.

In order to build complex RecyclerViews easily Airbnb made Epoxy. It can be used to make static pages with RecyclerView as well.

Before we go into sample implementation, feel free to follow me here and on Twitter. Now that self promotion is completed let's dig into code.

Contents

  • Basic implementation

  • Carousel

  • Epoxy with data binding

  • Conclusion

Basic Implementation

Let’s get started by adding Epoxy dependency and annotation processor to our app Gradle file.

// For Kotlin users apply kapt plugin
apply plugin: ‘kotlin-kapt’

kapt {
    correctErrorTypes = true
}

implementation ‘com.airbnb.android:epoxy:<latest-version>’
kapt 'com.airbnb.android:epoxy-processor:<latest-version>'

Epoxy has two main components EpoxyModel and EpoxyController

  • EpoxyModel is essentially our RecyclerView.ViewHolder ; it describes how our view should look like in RecyclerView.

  • EpoxyController is where we use generated models to show the data.

So let’s start by creating a model. Models are generated by Epoxy based on the given view or layout. Generated models are suffixed by _ (underscore) and can be used directly in EpoxyController.

To create a view holder model, extend EpoxyModelWithHolder. It takes the view holder type. View holder class must extend EpoxyHolder. Model properties are defined by annotating the field with @EpoxyAttribute. It generates a necessary setter that can be used to modify the field. You should never modify the fields directly.

@EpoxyModelClass(layout = R.layout.view_holder_message_item)
abstract class MessageItemModel : EpoxyModelWithHolder<MessageItemModel.Holder>() {

    @EpoxyAttribute
    lateinit var message: Message

    override fun bind(holder: Holder) {
        super.bind(holder)
        with(message) {
            holder.title.text = username
            holder.content.text = content
        }
    }

    class Holder : EpoxyHolder() {

        lateinit var profileImage: ImageView
        lateinit var title: AppCompatTextView
        lateinit var content: AppCompatTextView

        override fun bindView(itemView: View) {
            profileImage = itemView.findViewById(R.id.message_profile_image)
            title = itemView.findViewById(R.id.message_profile_name)
            content = itemView.findViewById(R.id.message_content)
        }
    }
}

Once your project build is successful, Epoxy will generate MessageItemModel_ and since we are using Kotlin it will also generate EpoxyModelKotlinExtensions.kt with extension functions for building the models. Similarly we create a HeaderItemModel

Now that models are created, let’s create a controller. For that we create a new class and extend EpoxyController and implement buildModels method.

class HomeController : EpoxyController() {
    
    override fun buildModels() {
        
    }
}

buildModels method is where we create new instance of the generated models or use AutoModels. In order to add the models to the controller we can call either EpoxyController#add(EpoxyModel) or EpoxyModel.addTo(EpoxyController). But since we are using Kotlin we can use the generated extension functions to build the models. They will automatically add the model to the controller.

class HomeController : EpoxyController() {

    var allMessages: List<Message> = emptyList()
        set(value) {
            field = value
            requestModelBuild()
        }

    override fun buildModels() {
        headerItem {
            id("all_messages")
            title("All Messages")
        }

        allMessages.forEach {
            messageItem {
                id(it.id)
                message(it)
            }
        }
    }
}

As you can see it was really easy to create models and have different view types in the RecyclerView . Just make sure you have unique IDs for the models or else a runtime exception is thrown. We are calling requestModelBuild() on setter to let the controller know the data is changed and models needs to be rebuilt. You can then use EpoxyRecyclerView and set the controller directly.

val controller = HomeController()
recyclerView.setController(controller)

We can then update the controller data by calling controller.allMessages = Data.messages - this will rebuild the models.

There are other EpoxyControllers you can use.

  • TypedEpoxyController : As the name suggests, it assigns a data type to controller. Instead of updating the data directly and calling requestModelBuild() we use setData(data) of controller to pass the object of the type we assigned. Here is our HomeController with TypedEpoxyController
class HomeController : TypedEpoxyController<List<Message>>() {
    
    override fun buildModels(listItems: List<Message>) {
        headerItem {
            id(“all_messages”)
            title(“All Messages”)
        }

        allMessages.forEach {
            messageItem {
                id(it.id)
                message(it)
            }
        }
    }
}

We can then update the controller data by calling controller.setData(newLisItems). You cannot call requestModelBuild() directly on typed controllers. If you want to pass more type parameters, we have Typed2EpoxyController, Typed3EpoxyController & Typed4EpoxyController respectively.

  • AsyncEpoxyController: By default all EpoxyControllers use main thread to build models and performing diffing. In order to do those in background thread we can simply use AsyncEpoxyController. Our HomeController now looks like this .
class HomeController : AsyncEpoxyController() {

    var allMessages: List<Message> = emptyList()
        set(value) {
            field = value
            requestModelBuild()
        }

    override fun buildModels() {
        headerItem {
            id("all_messages")
            title(“All Messages”)
        }

        allMessages.forEach {
            messageItem {
                id(it.id)
                message(it)
            }
        }
    }
}

We can then update the controller data by calling controller.allMessages = Data.messages.

As you can see from the examples above, building different view types using Epoxy is really easy. But what if you want have a horizontal RecyclerView above the existing list? You can use the inbuilt CarouselModel for this. It comes with common defaults and performance improvements.

Adding a carousel is really easy as well. You just add CarouselModel to controller and pass models(List<EpoxyModel>) .

override fun buildModels() {
    headerItem {
        id("recently_active")
        title("Recently Active")
    }
    val models = recentlyActive.map {
        RecentlyActiveItemModel_()
            .id(it.id)
            .profile(it)
    }
    carousel {
        id(“recent”)
        padding(Carousel.Padding.dp(0, 4, 0, 16, 8))
        hasFixedSize(true)
        models(models)
    }

    headerItem {
        id("all_messages”)
        title(“All Messages”)
    }
    allMessages.forEach {
        messageItem {
            id(it.id)
            message(it)
        }
    }
}

This is how our RecyclerView will look like.

Data binding

If you’re the type of person who hates writing all that view holder code to generate models, don’t worry. Epoxy has support for data binding and can generate the necessary models automatically from the XML.

Let’s get started by adding epoxy data binding module.

implementation ‘com.airbnb.android:epoxy-databinding:<latest-version>’

Also make sure to enable data binding support

dataBinding {
	enabled = true
}

Now all you have to do is create a package-info.java in any package, typically in the package you want to have your generated models in. Then annotate it with either EpoxyDataBindingLayouts for explicitly passing the layout names as input parameters to the annotation or EpoxyDataBindingPattern for automatically getting the layouts based on the given layout prefix. We will be using EpoxyDataBindingPattern .

@EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = “view_holder”)
package dev.sasikanth.epoxysample;

import com.airbnb.epoxy.EpoxyDataBindingPattern;

Once your project is built successfully, Epoxy will automatically generate all the models from the XML.

You can add the generated models in controller like so:

override fun buildModels() {
    headerItem {
        id("recently_active")
        title("Recently Active")
    }
    val models = recentlyActive.map {
        RecentlyActiveItemBindingModel_()
            .id(it.id)
            .profile(it)
    }
    carousel {
        id("recent")
        padding(Carousel.Padding.dp(0, 4, 0, 16, 8))
        models(models)
    }

    headerItem {
        id(“all_messages”)
        title("All Messages")
    }
    allMessages.forEach {
        messageItem {
            id(it.id)
            message(it)
        }
    }
}

As you can see, not much has changed from before, but we no longer have to write the view holder code. Epoxy will generate it from the data binding layouts. For more information about Epoxy with data binding, you can read the docs here .

Conclusion

Epoxy makes it really easy to implement complex RecyclerViews . When combined with Kotlin and Data binding, it makes it even easier. Epoxy can not only used for complex RecyclerViews but can also be used to generate static pages with RecyclerViews. I really suggest you look at Epoxy docs as well.

You can find the source code for project here. It has 2 branches: master and databinding.