Skip to content

Epoxy Models

Eli Hart edited this page Apr 12, 2019 · 33 revisions

Overview

Epoxy uses EpoxyModel objects to decide which views to display and how to bind data to them. This is similar to the popular ViewModel pattern. Models also allow you to control other aspects of the view, such as the grid span size, id, and saved state.

Creating Models

There are several ways to have Epoxy generate your model classes.

Generated model classes are suffixed with _ to indicate that they are generated.

Model IDs

The RecyclerView concept of stable ideas is built into Epoxy, meaning each EpoxyModel should have a unique id to identify it. This allows diffing and saved state.

Models are assigned ids via the EpoxyModel#id(long) method. This id value would generally come from a database entry, such as the id of a user.

However, there are many cases where an EpoxyModel is not backed by a database object and doesn't have a clearly assignable id. In these cases you may use a string as a model's id.

model.id("header")

Alternatively, if you have EpoxyModel's represented by object's from different database tables there is a risk of colliding ids. If that is a concern you may use a string to namespace the id.

model.id("photo", photoId)
model.id("video", videoId)

There are other overloads of id that accept multiple numbers or strings to help you create custom ids as needed. These alternative options are hashed into a 64 bit long to create an id for you. The downside to this approach is that, since the id is computed via a hash, there is the chance for id collisions which will cause an error. However, since a 64 bit hash is used the chance of a collision is extremely small. Assuming an even spread of hashcodes, and several hundred models in the adapter, there would be roughly 1 in 100 trillion chance of a collision. To protected against collisions, EpoxyController supports fallback behavior when duplicates are detected.

Automatic IDs in EpoxyController

For models that represent static content (such as a header or loader) you can use the AutoModel annotation with the EpoxyController to automatically generate a unique id for you.

Model Properties

Each model holds data that it eventually binds to a view. This data is represented by properties:

  • In custom views a property is declared with a @ModelProp annotation on a setter.
  • In databinding layouts each variable represents a model property.
  • In view holders the @EpoxyAttribute field annotation declares a property.

Every property must be a type that implements equals and hashcode. These implementations must correctly identify when the property value changes. Epoxy relies on this for diffing. Your properties may not be updated on the view if Epoxy doesn't know they have changed. The exception to this is callbacks, such as click listeners, which you should generally apply the DoNotHash option to.

On the generated EpoxyModels each property has a setter and getter; when you create a new model you build it by setting the value for each property.

new MyModel_()
       .id(1)
       .title("title")

Id is a required property for every model.

Model State

A model's state is determined by its equals and hashCode implementations, which is based on the value of all of the model's properties.

This state is used in diffing to determine when a model has changed so Epoxy can update the view.

These methods are generated so you don't have to created them manually.

Creating Views

Each model is typed with the View that it supports. The model implements buildView to create a new instance of that view type. Additionally, getViewType returns an int representing that view type for use in view recycling.

When Epoxy receives the calls RecyclerView.Adapter#getItemViewType and RecyclerView.Adapter#onCreateViewHolder it delegates to these methods on the model.

These methods are generated for you unless you are creating models manually.

Binding Views

When RecyclerView.Adapter#onBindViewHolder is called, Epoxy delegates to the model at the requested position with EpoxyModel#bind(View). This is the model's chance to populate the view with the properties in the model.

Similarly, Epoxy delegates RecyclerView.Adapter#onViewRecycled to EpoxyModel#unbind(View), giving the model a chance to release any resources associated with the view. This is a good opportunity to clear the view of large or expensive data such as bitmaps.

Since RecyclerView reuses views when possible, a view may be bound multiple times, without unbind necessarily being called in between. You should make sure that your usage of EpoxyModel#bind(View) completely updates the view according to the data in your model.

Generated models have an onBind method added to them, which you can use to register a callback for when the model is bound to a view.

model.onBind((model, view, position) -> // Do something!);

Similarly, onUnbind can be used for an unbind callback.

These are useful if you need to take action when a view enters or leaves the screen.

View Updates

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.

If you are using models generated from custom views or databinding, then the partial rebind is done automatically for you.

If you are using manually created models you can implement EpoxyModel#bind(T view, EpoxyModel<?> previouslyBoundModel) to check which properties changed and update the view as needed.

Note: This only works with EpoxyController, not the legacy EpoxyAdapter class.

Click Listeners

If a model has a property of type View.OnClickListener then the generated model will include an additional, overloaded method for setting that click listener. The overloaded method will take a OnModelClickListener parameter.

This listener is typed with two parameters: the model's type and the model's view type. Implementations must implement void onClick(T model, V parentView, View clickedView, int position). This callback gives you the model whose view was clicked, the top level view (or view holder if you are using EpoxyModelWithHolder), the view that received the clicked, and the position of the model in the adapter.

You should use the position provided by this listener instead of saving a reference to the model's position when it was added to the adapter/controller. This is because if the model is moved RecyclerView does an optimization to not rebind the new model since it knows the data did not change. This also means that the model returned by onClick is not necessarily the most recently created model, if the model did not need to be rebound to the view.

OnCheckedChangeListener

Similarly, if a model has a property of type CompoundButton.OnCheckedChangeListener then an overloaded method will be generated on the model of type OnModelCheckedChangeListener - to provide access to the model and position when the listener is triggered.