EPOXY: A Short hand for RecyclerView

In android there is a widely used view for list, previously it was ListView now it’s RecyclerView The RecyclerView promised of optimised memory usage when the lists were huge, and it did performed well, BUT…

There came the unnecessary complexity and verbosity with our beloved RecyclerView . We all have struggled with it’s adapter and viewholder and if we require more optimisation we also have operated on the level of recyler view pools.

Doesn’t it seems a bit unnecessary for just displaying list? The code that displays list is pretty simple in javascript, flutter or any other popular framework, why does it have to be only in android that displaying list is such a hefty task. Moreover, we still have not talk about the nested lists and list varying items (OMG).

At least in my opinion it’s a headache to implement a recycler view inside a view holder, how about 3–4 level of nesting, you could imagine the complexity and verbosity associated with.

Now, here comes the AirBnB’s epoxy that delegates most of the complexity, optimisations and verbosity related to recycler view of android. Here is the link to this library; https://github.com/airbnb/epoxy

Epoxy is an Android library for building complex screens in a RecyclerView. Models are automatically generated from custom views or databinding layouts via annotation processing. These models are then used in an EpoxyController to declare what items to show in the RecyclerView.

This abstracts the boilerplate of view holders, diffing items and binding payload changes, item types, item ids, span counts, and more, in order to simplify building screens with multiple view types. Additionally, Epoxy adds support for saving view state and automatic diffing of item changes.

Enough of talk now, let’s go with Linus’s statement “Show me the Code”.

I’ve used this library with android data binding, hence there is neglible verbosity in my code. I’ve designed a screen that shows list of recipes and in between it also displays survey , inspirational quotes

Dependencies:

I’ve used ext.epoxyVersion = ‘4.4.4’

implementation "com.airbnb.android:epoxy:$epoxyVersion"
// Add the annotation processor if you are using Epoxy's annotations (recommended)
kapt "com.airbnb.android:epoxy-processor:$epoxyVersion"
implementation "com.airbnb.android:epoxy-databinding:$epoxyVersion"

STEP 1: Create necessary item layouts with consistent naming conventions, like in my case :

item_recipe_data = item to display recipe

item_survey_data = item to take answer from user via a simple question

item_inspiration_data = item to display an inspirational message

Use <com.airbnb.epoxy.EpoxyRecyclerView/> instead of RecyclerView

STEP 2: Create an abstract class or an interface EpoxyDataBindingConfig ;

@EpoxyDataBindingPattern(rClass = R::class, layoutPrefix = "item")
abstract class EpoxyDataBindingConfig {
}

This config class annotated with pattern will remove prefix from layout files’ name and create a model where you will be passing data, this data will bind with view while inflation occurs.

STEP 3: Build the project, the following models should be generated (when you use data binding)

recipeData for item_recipe_data

surveyData for item_survey_data

inspirationData for item_inspiration_data

STEP 4: Create a controller class that will operate on your data set and generated models from layout file names. Here is my controller for list;

class ExploreListController : TypedEpoxyController<List<Any>>() {

override fun buildModels(data: List<Any>) {
data.forEach {
when (it) {
is Recipe -> addRecipe(it)
is BooleanQuestion -> addBooleanQuestion(it)
is Inspiration -> addInspiration(it)
}
}
}

private fun addInspiration(itemData: Inspiration) {
inspirationalData {
id(itemData.hashCode())
data((itemData))
}
}

private fun addBooleanQuestion(itemData: BooleanQuestion) {
surveyData {
id(itemData.hashCode())
data(itemData)
}
}

private fun addRecipe(itemData: Recipe) {
recipeData {
id(itemData.hashCode())
data(itemData)
}
}
}

In the above code for when block each case will produce different items

Each item needs a unique identifier in order to differentiate between items, I’ve used here a simple hash code you may use whatever field as your business requires. The itemData that is been passed to data as data(itemData) , the data is actually a variable from layout file that is to be data bound. itemData is our domain model that is initialises the data in layout file.

Here is my layout file for better understanding;

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="data"
type="com.goodfood.app.models.domain.Inspiration" />
</data>

<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_size_20"
app:cardCornerRadius="@dimen/dimen_size_5">

// The `data` will be consumed in views as
// normal databinding, this `data` is initiated by data(itemData)
</androidx.cardview.widget.CardView>

</layout>

The similar is true for other 2 layout files.

STEP 5: Finally, we pass data from our Activity/Fragment into the controller;

I’ve used fragment and initialized the list in Fragment’s onViewCreated()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val list = mutableListOf<Any>()

list.add(Recipe())

list.add(Inspiration())

list.add(BooleanQuestion())

val controller = ExploreListController()
controller.setData(list)
binding.recyclerRecipes.setController(controller)
}

Now, I’ve put the empty models just to make the UI in this medium post be pretty concise, but, you will need to initialise the models as per your requirements.

STEP 6(Optional): Now, if you want to update a single item in list, here you can’t do it directly as in our conventional approach for recyclerview. Here, immutability is taken seriously and prevents developer to mess up the lists when multithreading comes into picture.

In order to update an item you need to create an EpoxyModel in my case it’s DataBindingEpoxyModel for obvious reasons.

@EpoxyModelClass
abstract class RecipeModel : DataBindingEpoxyModel() {
var recipe: Recipe? = null
var clickListener: IClickListener? = null

override fun getDefaultLayout(): Int {
return R.layout.item_recipe_data
}

override fun setDataBindingVariables(binding: ViewDataBinding?) {
binding as ItemRecipePhotoDataBinding
binding.data = recipe
binding.clickListener = clickListener
}
}

This generates a model RecipeModel_() , you need to add this to controller in following manner; (My addRecipe() is changed please see above controller code)

private fun addRecipe(itemData: Recipe) {
val model = RecipeModel_().id(itemData.hashCode())
model.recipe = itemData
model.addTo(this)
}

And that’s it , Here is my screen :-

A Beautiful and smooth list that display recipes, user surveys & campaign

Thanks for reading my article, HAPPY CODING !!!

Android Developer at Silicus Technologies, Pune. I'm passionate about new technologies and innovations that happen in programming world, also a fan of Node.js

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store