Android RecyclerView DiffUtil

Filed Under: Android

In this tutorial, we’ll be discussing and implementing DiffUtil in our android application. If you haven’t implemented RecyclerView yet, refer this tutorial before proceeding ahead.

Android RecyclerView DiffUtil

DiffUtil is a utility class present with the Android Support Library. It’s a callback that detects changes between two Lists. It’s used as an alternative for notifyDataSetChanged() in our RecyclerView adapters.

notifyDataSetChanged is costly. Whenever invoked it updates each and every RecyclerView row. This is costly. DiffUtil does not call each and every row. Instead, it updates only those rows that are different between the Lists of values.

DiffUtil is a utility class that can calculate the difference between two lists and output a list of operations that converts the first list into the second one.

DiffUtil uses the following methods of the RecyclerViewAdapter:

  • notifyItemMoved()
  • notifyItemRangeChanged()
  • notifyItemRangeInserted()
  • notifyItemRangeRemoved()

These are less costlier than notifyDataSetChanged since they work on individual operations.

DiffUtil.CallBack class has the following methods that need to be implemented:

  • getOldListSize() : It returns the size of the old list.
  • getNewListSize() : Returns the size of the new list.
  • areItemsTheSame(int oldItemPosition, int newItemPosition) : We check if the individual items of the list are same. This can be done through checking their ids.
  • areContentsTheSame(int oldItemPosition, int newItemPosition): This checks if the contents of the List data are the same. This method is called by DiffUtil only if areItemsTheSame returns true.
  • getChangePayload(int oldItemPosition, int newItemPosition) : If areItemTheSame return true and areContentsTheSame returns false DiffUtil calls this method to get a payload about the change. Here we can detect if any particular field of the data is changed. We can then pass that changed value using Bundle. It’ll be received in our RecyclerView Adapter class.

Enough theory, let’s learn it by example now.

In the following section, we’ll create an application that shows dummy data of CryptoCurrency coins in our RecyclerView. Using two FloatingActionButtons, we can alter the data (add more data, modify current data).

Let’s dive right in!

Project Structure

android recyclerview diffutil project

Code

The code for the activity_main.xml layout is given below:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fabAddList"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_input_add" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fabChangeList"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_menu_sort_by_size" />

</RelativeLayout>

The code for the cardview_item_layout.xml file is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp">

        <TextView
            android:id="@+id/txtName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Bitcoin" />

        <TextView
            android:id="@+id/txtPrice"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="1000 USD" />

    </LinearLayout>

</android.support.v7.widget.CardView>

This hosts the RecyclerView rows.

Let’s look at our Model.java class:


package com.journaldev.androidrecyclerviewdiffutil;

public class Model implements Comparable, Cloneable {

    public String name;
    public int id, price;

    public Model(int id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    @Override
    public int compareTo(Object o) {
        Model compare = (Model) o;

        if (compare.id == this.id && compare.name.equals(this.name) && compare.price == (this.price)) {
            return 0;
        }
        return 1;
    }

    @Override
    public Model clone() {

        Model clone;
        try {
            clone = (Model) super.clone();

        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e); //should not happen
        }

        return clone;
    }

}

We’ve implemented the Comparable and Cloneable Interfaces.

They’ll be used to compare the objects in the DiffUtil and to create a deep copy of the ArrayList of Models that we’ll see next.

The code for the MainActivity.java class is given below:


package com.journaldev.androidrecyclerviewdiffutil;

import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {


    RecyclerView recyclerView;
    RecyclerViewAdapter recyclerViewAdapter;
    FloatingActionButton fabAddList, fabChangeList;

    private ArrayList<Model> modelArrayList = new ArrayList<>();

    public int i = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recyclerView);
        fabAddList = findViewById(R.id.fabAddList);
        fabChangeList = findViewById(R.id.fabChangeList);

        dummyData();
        recyclerViewAdapter = new RecyclerViewAdapter(modelArrayList);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.setAdapter(recyclerViewAdapter);

        fabAddList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addMoreCoinsToTheList();
            }
        });

        fabChangeList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changePricesInTheList();
            }
        });
        
    }

    private void dummyData() {

        modelArrayList.add(new Model(i++, "Bitcoin", 8000));
        modelArrayList.add(new Model(i++, "Ethereum", 600));
        modelArrayList.add(new Model(i++, "Litecoin", 250));
        modelArrayList.add(new Model(i++, "Bitcoin Cash", 1000));
    }

    private void addMoreCoinsToTheList() {
        ArrayList<Model> models = new ArrayList<>();

        for (Model model : modelArrayList) {
            models.add(model.clone());
        }
        models.add(new Model(i++, "Tron", 1));
        models.add(new Model(i++, "Ripple", 5));
        models.add(new Model(i++, "NEO", 100));
        models.add(new Model(i++, "OMG", 20));

        recyclerViewAdapter.setData(models);
    }

    private void changePricesInTheList() {

        ArrayList<Model> models = new ArrayList<>();
        
        for (Model model : modelArrayList) {
            models.add(model.clone());
        }

        for (Model model : models) {
            if (model.price < 900)
                model.price = 900;
        }
        recyclerViewAdapter.setData(models);
    }

}

ArrayList are passed by reference. We need to create a deep copy of the ArrayList to prevent the original ArrayList from changing when we modify the new ArrayList. Otherwise, DiffUtil would get two ArrayLists that are just the same.

recyclerViewAdapter.setData(models); is used to set the new data in the Adapter class.

The code for the RecyclerViewAdapter.java class is given below:


package com.journaldev.androidrecyclerviewdiffutil;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.CryptoViewHolder> {

    private ArrayList<Model> data;

    public class CryptoViewHolder extends RecyclerView.ViewHolder {

        private TextView mName, mPrice;

        public CryptoViewHolder(View itemView) {
            super(itemView);
            mName = itemView.findViewById(R.id.txtName);
            mPrice = itemView.findViewById(R.id.txtPrice);
        }
    }

    public RecyclerViewAdapter(ArrayList<Model> data) {
        this.data = data;
    }

    @Override
    public CryptoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview_item_layout, parent, false);
        return new CryptoViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(CryptoViewHolder holder, int position) {
        holder.mName.setText(data.get(position).name);
        holder.mPrice.setText(data.get(position).price + " USD");
    }

    @Override
    public void onBindViewHolder(CryptoViewHolder holder, int position, List<Object> payloads) {

        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            Bundle o = (Bundle) payloads.get(0);
            for (String key : o.keySet()) {
                if (key.equals("price")) {
                    holder.mName.setText(data.get(position).name);
                    holder.mPrice.setText(data.get(position).price + " USD");
                    holder.mPrice.setTextColor(Color.GREEN);
                }
            }
        }
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    public ArrayList<Model> getData() {
        return data;
    }

    public void setData(ArrayList<Model> newData) {

        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallBack(newData, data));
        diffResult.dispatchUpdatesTo(this);
        data.clear();
        this.data.addAll(newData);
    }
}

onBindViewHolder(CryptoViewHolder holder, int position, List<Object> payloads) gets triggered when the DiffUtil makes changes in the Adapter.

Inside it, we get the bundle value from the payload of the DiffUtil class. This contains the individual fields that were changed.

Inside the setData() we pass the new ArrayList.

dispatchUpdatesTo(this) invokes the adapter and informs it about the views to be updated.

Next, we copy the newData list to the data.

The code for the MyDiffUtilCallback.java class is given below:


package com.journaldev.androidrecyclerviewdiffutil;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.util.DiffUtil;
import java.util.ArrayList;

public class MyDiffUtilCallBack extends DiffUtil.Callback {
    ArrayList<Model> newList;
    ArrayList<Model> oldList;

    public MyDiffUtilCallBack(ArrayList<Model> newList, ArrayList<Model> oldList) {
        this.newList = newList;
        this.oldList = oldList;
    }

    @Override
    public int getOldListSize() {
        return oldList != null ? oldList.size() : 0;
    }

    @Override
    public int getNewListSize() {
        return newList != null ? newList.size() : 0;
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return newList.get(newItemPosition).id == oldList.get(oldItemPosition).id;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        int result = newList.get(newItemPosition).compareTo(oldList.get(oldItemPosition));
        return result == 0;
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        
        Model newModel = newList.get(newItemPosition);
        Model oldModel = oldList.get(oldItemPosition);

        Bundle diff = new Bundle();

        if (newModel.price != (oldModel.price)) {
            diff.putInt("price", newModel.price);
        }
        if (diff.size() == 0) {
            return null;
        }
        return diff;
        //return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

“price” is the payload data that was passed to the Adapter.

We can then animate/change text color of the field to indicate the user that it was modified.

The output of the above application in action is given below:

android recyclerview diffutil output

Here we’ve changed the text color of the rows in which the price was modified.

It’s just a random data. Wish the actual crypto market looks like this someday too!

This brings an end to this tutorial. You can download the project from the link below:

You can also download Android Studio project from our GitHub Repository.

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages