How to build your complex UI with Epoxy
Try to imagine a world where building complex scrollable UI is fun and not torment—the safe place where you don’t have to write the same configuration patterns again and again.
Yes, writing RecyclerView.Adapter and RecyclerView.holder went through my head too.
Believe it or not, such a world already exists thanks to the open-source Android library, Epoxy.
Our team has been using it in projects for almost two years now, and we are thrilled with the optimization it provides us.
So, if you also want to make your life easier and dive in that amazing world, keep reading.
And if you’re looking for someone to help you out with Android development on your next project – feel free to reach out to us! We’d like to help!
P.S. If you’re leaning more into iOS development, you’ll love our articles on supporting different screen sizes with a custom collection view layout and learning AR and ARKit.
What we have prepared for you today
- Epoxy, tell me more about it
- I am in, show me an example
- Add dependencies to Library
- ViewHolders bye, EpoxyModels hi
- Create a controller
- Handle user interactions
- Connect RecyclerView and controller
- Item changes, Epoxy win
- Over to you now!
Epoxy, tell me more about it
The Airbnb team designed Epoxy in 2016, and since then is upgrading.
Thanks to the excellent quality and detailed documentation, Epoxy has won many developers’ hearts, which is confirmed by more than six and a half thousand stars given to the repository.
What provoked the Airbnb team to write that lib?
Good question! This is what they said:
“We developed Epoxy to simplify the process of working with RecyclerViews, and to add the missing functionality we needed.”
Interesting idea, but I won’t believe it until I don’t see how they achieve that?
Okay, let me explain to you as fastest as I can.
Behind Epoxy’s scene is RecyclerView. So, to understand how it works, I will first remind you of classes necessary to build a scrollable list with RecyclerView.
Besides the RecyclerView object added to the layout, for building a scrollable list, we need two more objects:
- RecyclerView.ViewHolder – for displaying particular list item with a view
- RecyclerView.Adapter – for managing view holders and binding their data.
Epoxy provides us complete abstraction of these two classes, and their replacements are:
- EpoxyModel – abstraction for the RecyclerView.ViewHolder, it contains info about item’s layout and handles binding data to the item’s view
- EpoxyController – abstraction for the RecyclerView.Adapter, a straightforward interface that holds all models and places them as needed inside RecyclerView.
To demonstrate to you how these classes optimized our code and spared us unnecessary headaches, I created a small sample project.
So, let’s dive into it.
I am in, show me an example
The image below is the design of one complex scrollable screen.
As a developer, my task is to create that screen with as little code as possible, keeping in mind that the other screens may reuse these building elements.
This screen could be built with ScrollView, but I don’t recommend it for several reasons.
First is performance: hiding, showing, and updating views doesn’t come with smooth animations, as is the case with RecyclerView.
Second is that the layout’s complexity increases over time, and it is hard to maintain and evolve it.
Also, the reusability of elements is questionable.
Epoxy goes beyond all these problems.
Because it is an abstraction over RecyclerView, we can expect the same performances that RecyclerView has plus lots of extra features.
We also like Epoxy because of syntax and Kotlin support.
I want to emphasize that this project sample is written in Kotlin, and if you are not familiar with that programming language, it can be a bit difficult for you to follow.
But no worries, Kotlin is very easy to learn.
P.S. If you’d like to learn more about Android development – check out our article on RxJava!
Add dependencies to Library
Add these dependencies to your project build.gradle file.
I’ll link the latest version number you can find here.
Also, you need to add a plugin: ‘kotlin-kapt’ and kapt { correctErrorTypes = true }.
In the last two projects, we used data binding to set up views and handle user interactions and found it very beneficial for our development.
Knowing that Epoxy has support for data binding was a big encouragement to make that step.
To enable data binding in all your modules, add “android.defaults.buildfeatures.databinding = true” to the gradle.properties.
To be able to generate epoxy models from data binding add epoxy databinding dependency.
ViewHolders bye, EpoxyModels hi
To achieve a high level of elements reusability, I divided the screen into nine unique views.
We have a header image, title, subtitle, section title, chip group, info, cart, description, and item.
Each one is represented with an XML layout that contains a data tag with the necessary binding variable used to set data to cell views.
After all cell layouts, with belonging data classes, are created – it’s time to bind them with a class that will know how to display each inside RecyclerView.
That is where the Airbnb team freed us up a lot of work because Epoxy can automatically generate EpoxyModels from databinding XML layouts which, I think, is fantastic.
Epoxy gives us the ability to generate databinding models for a group of layouts at once or explicitly just for one particular layout.
In this project sample, I used the first approach that is called automatic based on the naming pattern.
Epoxy will generate a databinding model for every layout file that starts with a given prefix; in my example, it is “cell”.
Add this snippet of code and build project.
After the project is built successfully, Epoxy will automatically generate all the models from the XML.
Each module’s layout variable will have a setter and will be bound when the model is attached.
Create a controller
We have prepared our epoxy models, now it is time to declare them and set to screen.
Here is where EpoxyController comes to play. We can think of EpoxyController as an immutable class that is an interface between data and view.
To change state is necessary to have a callback because the view cannot update or modify an EpoxyModel.
There are several types of EpoxyControllers.
In our projects, we use TypeEpoxyController, which assigns a data type to it.
We mainly use it because it is prone to type safety and has a simplified way of updating the data through method setData(data).
Inside the controller, we have to override the method buildModels.
Well, it is not hard to guess that we should declare models inside that method.
To determine which of the epoxyModels should be created, we use model-specific data classes with BaseCell as parent class.
To create an instance of the model, we need to remove the prefix used to generate the models from the layout file name.
For example, to create the title model, we removed the prefix “cell” from its layout file name (cell_title.xml), and that left us with the just title.
To ensure that the controller will work properly, we need to pay attention to these two things when creating EpoxyModels:
- All models must set a unique id.
- Models are immutable, and their state cannot be changed once they are added to the controller.
Handle user interactions
You may be wondering, “And what about user interaction and how we handle them?”
As I mentioned earlier, you need to have the callback to make the change inside the EpoxyModel.
In my example, it is EpoxySampleControllerListener. We set ViewModel to implement that interface because it can update UI data in the first place.
To EpoxyModels that need to handle user interaction, like clicks or so, we add a listener variable inside the layout file.
After the project rebuild, the generated model class will get setter for that variable too.
Connect RecyclerView and controller
Now comes the easiest part of all: setting up the RecyclerView with the controller.
Add EpoxyRecyclerView to the fragment layout and then inside fragment’s onViewCreated method, set the controller to recyclerView with adding “binding.epoxyRecyclerView.setController(controller)”.
To set or update the controller’s data, use the setData(data) method.
Item changes, Epoxy win
Remember how we have to notify RecyclerView.Adapter whenever an item was added, deleted, updated, or moved to work correctly.
Handling these cases often brought part of the business logic inside the Adapter that significantly increased code complexity.
Epoxy tackled this problem by using a diffing algorithm on our models.
Whenever we set controllers data with the updated dataset, Epoxy detects models affected by changes and reports that to RecyclerView.
It is hard to emphasize how beneficial this is. Our controller has only one job, and that is to connect data with models.
We have change animations with no developer effort, and performance is highly improved because views are rebound only when necessary.
Over to you now!
I hope this blog gave you an idea of how Epoxy and data binding represents a killer combination for creating list-based views for both static and dynamically-loaded content.
And here’s the source code for the project sample.
If you already worked with Epoxy, please share your experience with us and give us some tips.
If not, I encourage you to dive into this more and look at Epoxy docs as well.
Also, I challenge you to use Epoxy with data binding to build a complex scrollable screen that will display interesting and helpful information about the place, city, or village that you like.
Let your imagination run wild and create as many different elements for the screen.
Send us your solution on hello@factory.dev, and we will be happy to review it and give you guidelines for further learning.