Back to Mavericks

Epoxy

docs/epoxy.md

3.1.03.9 KB
Original Source

Epoxy

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's Philosophy

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 Mavericks sample app):

kotlin
fun invalidate() {
    recyclerView.requestModelBuild()
}

fun buildModels() = withState(viewModel) { state ->
  header {
    id("header")
    title("Hello World")
  }

  state.items.forEach { item ->
    itemCard {
        id(item.id)
        title(item.title)
        clickListener { _ ->
            navigateToItemDetails(item.id)
        }
    }
  }
}

Combining with Mavericks

The model building pattern of Epoxy enforces a one way data flow. When a state change occurs, models are rebuilt asynchronously and Epoxy runs a background diff to figure out what changed. Changes are then dispatched on the main thread to the RecyclerView. This fits in perfectly with the reactive, state based approach of Mavericks.

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. Mavericks 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)

kotlin
abstract class BaseEpoxyFragment : Fragment(R.layout.base_epoxy_fragment) {

    abstract val epoxyController: EpoxyController

    override fun onViewCreated() {
        recyclerView.setController(epoxyController)
    }

    override fun invalidate() {
        recyclerView.requestModelBuild()
    }
}

We can then create a new page very easily be extending our base fragment:

kotlin
class HelloWorldEpoxyFragment : BaseFragment() {
    private val viewModel: HelloWorldViewModel by fragmentViewModel()

    override fun epoxyController() = simpleController(viewModel) { state ->
        marquee {
            id("marquee")
            title(state.title)
        }
    }
}

Benefits of Epoxy

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 Mavericks.

  • Epoxy supports building models and diffing on a background thread. The base fragment Mavericks 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 itemCard, 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 Mavericks fragment.
  • Epoxy integrates with Paris to allow full styling control of your views through the model declaration.