-
Notifications
You must be signed in to change notification settings - Fork 500
Epoxy & MvRx
Epoxy is an open source library by Airbnb that makes it easier to build complex layouts. It builds on top of RecyclerView, and allows you to work with multiple view types in a declarative, reactive way.
Epoxy uses immutable "models" to describe the interface between data and view. An EpoxyModel
class exists for each unique view in your layout, and defines how the view is created, styled, and bound to data. Epoxy provides a powerful annotation processor to generate these models for you based on custom views or databinding layouts.
To create the layout of a page, EpoxyModels are declared in the order that views should appear, with the data that should be bound to them. This is called "building models". Since the models are immutable they must be rebuilt whenever the data changes to provide a new snapshot that describes the view.
This looks like this (adopted from the MvRx sample app):
fun buildModels() {
marquee {
id("marquee")
title("Dad Jokes")
}
state.jokes.forEach { joke ->
basicRow {
id(joke.id)
title(joke.joke)
clickListener { _ ->
navigateTo(
R.id.action_dadJokeIndex_to_dadJokeDetailFragment,
DadJokeDetailArgs(joke.id)
)
}
}
}
loadingRow {
onBind { _, _, _ -> viewModel.fetchNextPage() }
}
}
The model building pattern of Epoxy enforces a one way data flow. When a state change occurs, models are rebuilt and Epoxy runs a diff to figure out what changed. This fits in perfectly with the reactive, state based approach of MvRx.
A pattern we use at Airbnb, and that we share in the sample app, is that each fragment contains an EpoxyController that defines how models are built. MvRx automatically manages this, and rebuilds the models whenever the state changes and the view is invalidated. This can be built into a base fragment, so that individual feature fragments have very little overhead in declaring a new page.
An abridged version looks like this: (The complete code can be found in the sample app)
abstract class BaseFragment : BaseMvRxFragment() {
protected lateinit var recyclerView: EpoxyRecyclerView
protected val epoxyController by lazy { epoxyController() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_base_mvrx, container, false).apply {
recyclerView = findViewById(R.id.recycler_view)
recyclerView.setController(epoxyController)
}
}
override fun invalidate() {
recyclerView.requestModelBuild()
}
abstract fun epoxyController(): MvRxEpoxyController
}
We can then create a new page very easily be extending our base fragment:
class HelloWorldEpoxyFragment : BaseFragment() {
private val viewModel: HelloWorldViewModel by fragmentViewModel()
override fun epoxyController() = simpleController(viewModel) { state ->
marquee {
id("marquee")
title(state.title)
}
}
}
While Epoxy gives us a pretty syntax for declaring UI, it has a lot of other things going on under the hood that improve its usefulness with MvRx.
- Epoxy supports building models and diffing on a background thread. The base fragment MvRx uses provides this automatically so all of the Epoxy rendering happens off the main thread. You can trigger a large amount of state changes and not have to worry about performance.
- Epoxy only updates the parts of a view that changed. If a state change resulted in an updated header title, only the header text of your view is rebound. Only your picture changed? Great, nothing else needs to be updated.
- The functions in the example code for declaring a view, such as
marquee
, are Kotlin extension functions generated by Epoxy. Epoxy generates almost all the boilerplate for you, so you can simply create an xml layout with databinding and use it immediately in a MvRx fragment. - Epoxy integrates with Paris to allow full styling control of your views through the model declaration.