Simplifying Recycler View with Epoxy in Kotlin — Nachos Tutorial Series

Navendra Jha
AndroidPub
Published in
7 min readSep 4, 2018

Recycler View — Inarguably one of the most used view component in Android, but still setting it up and modifying it later as per design change is quite hectic and that’s what Airbnb’s new library which they call as their View Architecture is solving. Meet Epoxy!

In few words, Epoxy removes lot of boilerplate codes needed for setting up RecyclerView / any adapter based view and also provides a lot of interfaces to bring fast adaptation to design changes and to make the code more modular and manageable.
For full technical and design introduction to Epoxy check out this link where I have tried to introduce Epoxy in simpler way and have also added links and resources to learn more about it.

Disclaimer

This is not the best example of Epoxy. Epoxy works best when there’s complex recycler view. Kindly check out Epoxy’s Github page for the example. This article instead is a simple introduction to Epoxy for recycler view and can be used for base code for developing complex views.

Cool, let’s jump directly to code then!

We are going to build a simple RecyclerView showing list of various kind of foods with a image, title and description. We will be using Epoxy as our View library and Kotlin as development language.

GitHub Link

If you directly want to check the full implementation, then jump to the Github link.

Detailed Explanation

Start a new Android project with default settings and Kotlin support checked in. It will create all the necessary libraries and gradle file. We will first add epoxy and RecyclerView library to the project. See below.

Add Support Library & setup project

In your build.gradle file for app (not for root project), add following libraries and appropriate versions. For current gradle version of epoxy go to Epoxy’s Github page. Also add Kapt { correctErrorTypes = true} and apply plugin: ‘kotlin-kapt’.

apply plugin: 'kotlin-kapt'//Added for Epoxy
kapt {
correctErrorTypes = true
}
dependencies {
ext.epoxyVersion = 2.8.0
//Android RecyclerView
implementation 'com.android.support:recyclerview-v7:27.1.1'

//Airbnb Epoxy
implementation "com.airbnb.android:epoxy:$epoxyVersion"
kapt "com.airbnb.android:epoxy-processor:$epoxyVersion"
}

Remember to use kapt instead of annotationProcessor for any kotlin project as many libraries work on annotation processing and so they won’t work properly if not used with kapt.

Now create three packages in the main directory:

  1. Models — We will keep all our models, data factory and objects here.
  2. Views — We will keep all our views, activities and ui related stuff here
  3. ViewModel — ViewModels are like bridge between models and views and so we will keep our view models or Epoxy Controllers here.

Let’s Prepare UI for our App

Let’s start with views of our app and our end result would look something like the following figure:

Single Viewtype Recycler-View built using Epoxy

This is a scrollable view with each item having one image, one title and one text. The scrollability is achieved by RecyclerView component. So let’s add a RecyclerView to our activity_main.xml file as shown below:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".views.MainActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</android.support.constraint.ConstraintLayout>

Now create a new layout file as singlefood_layout.xml as shown below:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/image"
android:layout_width="180dp"
android:layout_height="141dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.049"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.05"
app:srcCompat="@mipmap/ic_launcher" />

<TextView
android:id="@+id/title"
android:layout_width="119dp"
android:layout_height="20dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:text="TextView"
android:textAllCaps="false"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.376"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.038" />

<TextView
android:id="@+id/desc"
android:layout_width="144dp"
android:layout_height="96dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/nachosDesc"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.75"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintVertical_bias="0.019" />
</android.support.constraint.ConstraintLayout>

This singlefood_layout file is a view for a single food item and we will use Epoxy to inflate this view with appropriate data and will put it in above declared RecyclerView at the appropriate position.

Add Models and data factory

Lets first define our Food model which is going to be a Kotlin’s data class as shown below and it will hold data values for one instance of a food.

Food.kt

data class Food (
val image:Int=-1,
val title:String="",
val description:String=""
)

This is a normal Kotlin data class of Food with properties like image as Int, title as String and description as string. This is just as normal class loaded with some extra utility functions such equals()/hashCode(), toString() etc.

SingleFoodModel.kt

Now as we know, we need a adapter extended from RecyclerView.Adapter for binding a particular view to a view holder and then passing data to it. In Epoxy, this is handled by EpoxyModels. Like Adapters it also have ViewHolder class which is extended from EpoxyHolders.

@EpoxyModelClass(layout = R.layout.singlefood_layout)
abstract class SingleFoodModel : EpoxyModelWithHolder<SingleFoodModel.FoodHolder>(){

@EpoxyAttribute
var id : Long = 0

@EpoxyAttribute
@DrawableRes
var image : Int = 0

@EpoxyAttribute
var title:String? = ""

@EpoxyAttribute
var desc:String = ""


override fun bind(holder: FoodHolder) {
holder.imageView?.setImageResource(image)
holder.titleView?.text = title
}

inner class FoodHolder : EpoxyHolder(){

lateinit var imageView:ImageView
lateinit var titleView: TextView
lateinit var descView:TextView

override fun bindView(itemView: View?) {
imageView = itemView?.image
titleView = itemView?.title
descView = itemView?.desc
}

}
}

As seen in above code, this Model class is like what we have in RecyclerView.Adapter class. It contains a view holder class which in this case is FoodHolder class and is derived from EpoxyHolder. It holds the view elements defined in singlefood_layout.xml file and is responsible for holding the view elements of one particular view. Also this class needs to be abstract as Epoxy creates the ModelClass by its own and have naming convention of CustomModelClass_(). Once you write this abstract class, hit rebuild project and the CustomModelClass_() will be generated automatically. This automatic generation is achieved by KotlinPoet and JavaPoet for Kotlin and Java classes respectively.

There are number of ways to create an EpoxyModel class and we will be using them later in this blog series but for this particular blog we have used EpoxyModelWithHolder<ViewHolder> class. The EpoxyModel class holds a particular view object and the data variables for inflating the view. It doesn’t have data values and they are injected later by EpoxyControllers.

FoodDataFactory.kt

Now we need some data to display in our app. Technically this should come from some remote API but to keep the simplicity of our app, let’s just create a Data Factory with random data generators.

object FoodDataFactory{

//region Random Data Generators
private val random = Random()

private val titles = arrayListOf<String>("Nachos", "Fries", "Cheese Balls", "Pizza")

private fun randomTitle() : String {
val title = random.nextInt(4)
return titles[title]
}

private fun randomPicture() : Int{
val grid = random.nextInt(7)

return when(grid) {
0 -> R.drawable.nachos1
1 -> R.drawable.nachos2
2 -> R.drawable.nachos3
3 -> R.drawable.nachos4
4 -> R.drawable.nachos5
5 -> R.drawable.nachos6
6 -> R.drawable.nachos7
else -> R.drawable.nachos8
}
}
//endregion

fun getFoodItems(count:Int) : List<Food>{
var foodItems = mutableListOf<Food>()
repeat(count){
val image = randomPicture()
val title = randomTitle()
@StringRes val desc = R.string.nachosDesc
foodItems.add(Food(image,title,desc))
}
return foodItems
}
}

As you can see, the FoodDataFactory has two private methods to generate random title and random image. For image, downloads few food images from internet and put them in res/drawable folder and reference to them in randomPictures() function. I have left the randomDescription() for simplicity and directly setting it in xml file. Ideally, all these should come from some api endpoints.

Let’s Write Controller / ViewModel

The ViewModel acts as bridge between models & view and handles proper flow of data between them. In Epoxy, EpoxyControllers act as bridge between views and EpoxyModel.

SingleFoodController.kt

class SingleFoodController : EpoxyController(){

var foodItems : List<Food>

init {
foodItems = FoodDataFactory.getFoodItems(50)
}

override fun buildModels() {
var i:Long =0

foodItems.forEach {food ->
SingleFoodModel_()
.id(i++)
.image(food.image)
.title(food.title)
.addTo(this)
}
}

}

This class is extended from EpoxyController class and needs to override buildModels() method in which adds model/models to this controller. For this particular example, I have just added one single model but later in this blog series I have created more complex RecyclerViews with multiple view types and models. Also we need to add id to every model so that Epoxy can optimise the RecyclerView adaptation.

Setting it up all together

Now we are ready to setup the created functionalities in our recycler view. For that changed the MainActivity.kt file as shown:-

class MainActivity : AppCompatActivity() {

private val recyclerView : RecyclerView by lazy { findViewById<RecyclerView>(R.id.recycler_view) }

private val singleFoodController : SingleFoodController by lazy
{ SingleFoodController() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecycler()
}

private fun initRecycler(){
val linearLayoutManager = LinearLayoutManager(this)
recyclerView.apply {
layoutManager = linearLayoutManager
setHasFixedSize(true)
adapter = singleFoodController.adapter
addItemDecoration(DividerItemDecoration(this@MainActivity, linearLayoutManager.orientation))
}

//This statement builds model and add it to the recycler view
singleFoodController.requestModelBuild()
}
}

We have created two variables to keep references of RecyclerView from activity_main.xml file and SingleFoodController loaded lazily.
We then have written a initRecycler() method which just initialize the RecyclerView with default values. For our particular case let’s add linearLayoutManager and for adapter set it up to singleFoodController.adapter. In the end call requestModelBuild() method of the controller to build models and inflate RecyclerView. This method needs to be called whenever there’s a change in the data.

Hit Run

Now hit run and you will see the app with scrollable list of foods. So you have seen how easy it is to create RecyclerViews with Epoxy. But this example does not show the power and beauty of Epoxy at all. In my next blog of this series I will be writing about developing RecyclerViews with multiple view type and re using them in different activities. Stay tuned.

Happy Coding!

--

--