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 RecyclerView
s 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 ourRecyclerView.ViewHolder
; it describes how our view should look like inRecyclerView
. -
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 EpoxyController
s you can use.
TypedEpoxyController
: As the name suggests, it assigns a data type to controller. Instead of updating the data directly and callingrequestModelBuild()
we usesetData(data)
of controller to pass the object of the type we assigned. Here is ourHomeController
withTypedEpoxyController
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 allEpoxyController
s use main thread to build models and performing diffing. In order to do those in background thread we can simply useAsyncEpoxyController
. OurHomeController
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
.
Carousel
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 RecyclerView
s . When combined with Kotlin and Data binding, it makes it even easier. Epoxy can not only used for complex RecyclerView
s but can also be used to generate static pages with RecyclerView
s. 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
.