Taming partial Epoxy models update
I have been using Epoxy for more than a year. It’s an open-source Android library for building complex screens in a RecyclerView
developed by Airbnb. The main advantage of it is automatic intelligent diffing of item changes, so UI is updated smoothly and with no developer efforts.
Sounds cool! But I spent whole evening trying to update part of the cell without animated reloading of all items in the list. At the end it was very straightforward, so let me share my solution with you.
Sample project
Let’s consider an application for learning French for English-speaking users. We’ll have some simple phrases/words with English and French version that are displayed in a list. At the beginning French translations are hidden, but user can reveal all of them by pressing a button.
This sample project can be found on GitHub. Commits are self-explanatory and split by features. These are starter commits, which aren’t complicated, so we won’t waste time on them:
- Create a simple project with
MainActivity
: b2ee0c1 - Add
Epoxy
dependency inbuild.gradle
: 48a021c - Add
data class Phrase
andMockDataSource
that only returns the list of hardcoded phrases : 2cd2aef
Display phrases
In af12129 basic EpoxyController
and EpoxyModel
are added to MainActivity
as inner classes. Along with adding EpoxyRecyclerView
to XML and basic layout for a list item, this allows us to display phrases in UI.
This is important to mention that chosen approach for creating EpoxyModel
is one of a few available, but I prefer this particular one for simplicity of use and end-to-end Kotlin experience.
Every EpoxyModel
must have its unique id. In our case it’s generated like this: PhraseEpoxyModel — $phrase
.
Hide / reveal translation
If you still remember our intention, we need to allow the user to reveal (and then hide if needed) English translation of French phrases / words. This can be done by introducing a variable isTranslationDisplayed
into MyEpoxyController
and requesting models rebuild on each variable’s change.
But it won’t work like this, because we don’t take into account isTranslationDisplayed
in model’s id. That’s why we append it to each model’s id. Works like a charm, but… it’s blinking and scrolling to the top.
Solution
It took me 4 hours to find the magic wand — EpoxyModel
has another bind method dedicated to Partial Model Update:
bind(view: View, previouslyBoundModel: EpoxyModel<*>)
Exactly what we need to get rid of UI artefacts.
Important note: your
EpoxyModel
must be adata class
(equals
andhashcode
are correctly overriden) and you don’t need addisTranslationDisplayed
to model’s id, because it’s already added to model itself.
Why this works? Because, as official Epoxy documentation states:
When a model’s state changes, and the model is bound to a view on screen, Epoxy rebinds the view with only the properties that changed. This partial rebind is more efficient than rebinding the whole view.
Bonus
Let’s imagine that users want to be able to see the translation for specific phrase by tapping on it (true story 😉). It’s super easy with the latest implementation, see details in 8e2bfbd.