Android DiffUtil.ItemCallback areContentsTheSame with different IDs

Micheal Johnson picture Micheal Johnson · May 22, 2018 · Viewed 7.9k times · Source

I'm using a RecyclerView to display a list of items that are retrieved from a database as Java objects. One of the fields in the object is the ID of the item in the database, so that further operations can be performed with it. My areContentsTheSame implementation compares various fields in the objects to determine if the contents of the item has changed.

The problem is that sometimes when the data source is refreshed the ID of the item changes without the visible contents changing (i.e. the same items are removed from the database and then added again, changing their ID in the process). In this case, my current areContentsTheSame implementation returns true. But when areContentsTheSame returns true, the RecyclerView views are not re-bound (onBindViewHolder is not called for the items for which areContentsTheSame returns true), so the views still hold a reference to the old object which contains the old ID thus causing an error when trying to do something with that item (such as the user tapping on it to display it).

I tried making areContentsTheSame return false when the ID changes, forcing the views to be re-bound even though no visible change has taken place, but this causes the "item changed" animation to show even though the item has not visibly changed. What I want is a way to either force the views to be re-bound without areContentsTheSame returning false or a way for areContentsTheSame to return true and trigger the re-binding without the animation being shown.

Answer

Ben P. picture Ben P. · May 22, 2018

The best way to achieve what you want is to override the getChangePayload() method in your DiffUtil.Callback implementation.

When areItemsTheSame(int, int) returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change.

For example, if you are using DiffUtil with RecyclerView, you can return the particular field that changed in the item and your ItemAnimator can use that information to run the correct animation.

Default implementation returns null.

So, make sure that your areContentsTheSame() method returns false when the database id changes, and then implement getChangePayload() to check for this case and return a non-null value. Maybe something like this:

@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
    if (onlyDatabaseIdChanged(oldItemPosition, newItemPosition)) {
        return Boolean.FALSE;
    } else {
        return null;
    }
}

It doesn't matter what object you return from this method, as long as it's not null in the case where you don't want to see the default change animation be played.

For more details on why this works, see the answer to this question: https://stackoverflow.com/a/47355363/8298909