Skip to content

DoNotHash

Eli Hart edited this page Sep 12, 2018 · 7 revisions

Epoxy has a concept of Do Not Hash, where properties are not updated even when their hashcode changes (as opposed to normal properties that are rebound whenever their hash changes). This is set as an optional parameter in the annotations EpoxyAttribute and ModelProp.

This is a performance optimization. The intended use for this is callbacks (such as click listeners) which are usually anonymous, and such have a different hashcode every time the model is built. Without “Do Not Hash”, every time a model build happens the differ would recognize every model with a click listener as having changed, which is potentially many many models. These would then all be rebound by the recyclerview.

This generally works well, but there is a large gotcha. If the callback captures any variables in its closure they can be out of date when the callback is invoked.

For example, imagine a Animal object is used to create an EpoxyModel, and the on click listener for the model holds a reference to that Animal for use in the callback. If the animal changes, and the model is rebuilt, the new click listener (with a reference to the new Animal) will not be bound to the view, and when it is invoked the old (and incorrect) Listing will be used in the callback.

This is usually just a problem if your data is mutable.

Don't use DoNotHash if you don't need it

If you are not worried about the performance impact of rebinding click listeners, then just don't add DoNotHash as an option 👍 This means you should also avoid using @CallbackProp since that uses DoNotHash internally. This may be a completely appropriate solution for you and is the easiest.

Note that if you're using databinding, DoNotHash is enabled by default for any variable whose type does not implement equals and hashcode. See the databinding docs for details on how to change this default.

Epoxy's Solution

To keep the benefits of DoNotHash while avoiding the complications, Epoxy offers a OnModelClickListener interface that can be used instead of a normal View.OnClickListener. This listener provides the EpoxyModel, View, and adapter position when it is clicked. Epoxy's generated code has special handling for this click listener, and it guarantees that the most up to date EpoxyModel is provided on click.

If all data is stored in the model, then it can be retrieved in onClick and always be up to date. You may need to store additional data in the model just for use in the click callback.

The downside to this approach is that it only works for click listeners, and any other sort of callback cannot make use of it.

Important: If data captured in the on click listener changes, but does not change any other property on the model, the model will still not be rebound because epoxy does not know that something changed. Make sure that all data representing your state is captured in the model, and that all props correctly implement equals and hashcode.

Alternative Solution #1

Alternatively you can avoid capturing any state in your callback closures. For example, simply capture an ID of the object and callback to a central controller to report that the item with that ID was clicked. Leave it up to the central store to look up the most recent version of that item by ID and take the correct action.

Alternative Solution #2

If the above solutions don't work for your use case, you could use a KeyedListener. Which pairs your listener callback with a value type so that the callback is updated whenever that value changes. This is a general solution that you can tailor to your needs

 class KeyedListener<Key, Listener> private constructor(val identifier: Key, val callback: Listener)  {

    companion object {
        @JvmStatic
        fun <Key, Listener> create(identifier: Key, callback: Listener): KeyedListener<Key, Listener> {
            return KeyedListener(identifier, callback)
        }
    }

    // Only include the key, and not the listener, in equals/hashcode
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is KeyedListener<*, *>) return false
        return identifier == other.identifier
    }

    override fun hashCode() = identifier?.hashCode() ?: 0
}

Usage in the model/view would look like

@ModelProp(Option.NullOnRecycle)
fun setKeyedOnClickListener(listener: KeyedListener<*, OnClickListener>?) {
   setOnClickListener(keyedListener?.callback);
}

And usage when creating the model would look like

 animals.forEach {
   animalModel {
     id(it.id)
     keyedOnClickListener(KeyedListener.create(it) { v: View -> // do something with the animal }
   }
 }