DiffUtils : Improving performance of RecyclerView

Raj Suvariya
MindOrks
Published in
6 min readFeb 21, 2017

You all must have started using recyclerview instead of listview to improve the performance of the app. If you haven’t please go through our recyclerview tutorial first …

Difference between listview and recyclerview

Well, recyclerview is a just improved version of Listview. In ListView, if you don’t create viewholder class then it creates views for all the rows, which is not good for the performance of the app as it takes more time as well as more memory. In recyclerview creating viewholder is a must, so that only a few views are needed to show the data and it can be recycled when and where needed. As shown in the below image when we swipe up and there are no views left at the bottom then the top most view will be reused by updating its data.

RecyclerView can be improved further

Yes! you heard it right. We use recyclerview because it doesn’t create views for all the rows, instead, it creates views that can fill up the screen and few above and below, and just recycles it. But, RecyclerView can be further improved when it comes to change the existing data of the list. For example your WhatsApp contact list, when someone changes their status you convey the data change message using the following traditional method of RecyclerView and it updates the data of RecyclerView.

adapter.notifyDataSetChanged();

Then What’s the problem?

When we use notifyDataSetChanged() method it updates all the view (all visible view on screen and few buffer view above and below the screen). It’s inappropriate way of updating list if just a few rows have changed. For example, your friend changes the status on WhatsApp then only that row should be updated instead of all the rows.

How?

Android has provided a class called DiffUtil under version 7 support utils. As per android documentation, the definition of DiffUtil is as follow

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

That’s enough of theory! It’s Code time

Download or clone the following repository from github. Where I have provided a pre-built template as well as complete code. In the template, I have implemented recyclerview of contacts list and also added TODOs with numbers. In the rest of the tutorial, we will start with the template and implement all the TODOs and reach to the complete working code.

TODO (1): add a SwipeRefreshLayout and wrap RecyclerView inside it

Open activity_main.xml. Add SwipeRefreshLayout and wrap recyclerview inside it as follow:

<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

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

</android.support.v4.widget.SwipeRefreshLayout>

TODO (2): bind xml SwipeRefreshLayout with java objects

Open MainActivity.java. We need to bind the SwipeRefreshLayout in xml with the java object. Here’s the code.

public class MainActivity extends AppCompatActivity {
...
SwipeRefreshLayout swipeRefreshLayout;

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

// TODO(2): bind xml SwipeRefreshLayout with java objects

swipeRefreshLayout
= (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
...
}
}

TODO (3): writeCompareTo method

Open Model/Contact.java. This is contact object class. We have implemented Comparable interface to compare two Contact objects. As we have implemented Comparable interface we need to implement the unimplemented method compareTo(Object 0).

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

if (compare.getName().equals(this.name) && compare.getPhonenumber().equals(this.phonenumber)){
return 0;
}
return 1;
}

TODO (4): write your DiffUtilCallBack class

Open Utils/MyDiffUtilCallback.java. Now, extend it with DiffUtil.Callback and ovride these 5 methods

  1. getOldListSize() : return the size of old list
  2. getNewListSize() : return the size of new list
  3. areItemsTheSame() : return true if the two items are same
  4. areContentsTheSame() : return true if content of two item is same
  5. getChangePayload() : return the difference between two items

Like this…

public class MyDiffUtilCallback extends DiffUtil.Callback {
ArrayList<Contact> newList;
ArrayList<Contact> oldList;

public MyDiffUtilCallback(ArrayList<Contact> newList, ArrayList<Contact> 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 true;
}

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

@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
Contact newContact = newList.get(newItemPosition);
Contact oldContact = oldList.get(oldItemPosition);

Bundle diff = new Bundle();
if(!newContact.getName().equals(oldContact.getName())){
diff.putString("name", newContact.getName());
}
if(!newContact.getPhonenumber().equals (oldContact.getPhonenumber())){
diff.putString("phone", newContact.getPhonenumber());
}
if (diff.size()==0){
return null;
}
return diff;
}
}

Here, we don’t have any key parameter to check if the items are same. So, we are always returning true which invokes areContentTheSame() for every position. In areContentTheSame() we are comparing two Contact object and checking if the names and phone numbers are the same or not. If they are not then the getChangePayload() method will be called. Where we put the difference in Bundle as key-value pairs.

TODO (5): update the data

Open DataFactory/DataGenerator.java. Here, we are creating a list of contacts in getData method(). After looking at the data we think that there are no mistakes, but when we look at it closely, we can see that we forgot to add “CEO” tag with “Rajat Talesra”. That’s unforgivable mistake!!!. So let’s update it in getUpdateData() method. Also, let’s change one contact’s phone number.

public static ArrayList<Contact> getUpdatedData(){
ArrayList<Contact> contacts= getData();
contacts.get(1).setName("Rajat Talesra (CEO)");
contacts.get(0).setPhonenumber("9067250043");
return contacts;
}

TODO (6): complete onNewData method

Open Adapter/MyRecyclerViewAdapter.java. Here, we need to implement a method to let adapter know about the updated data. So complete the onNewData method by calling the MyDiffUtilCallBack class and getting the difference between old and new list and dispatching it to the adapter.

public void onNewData(ArrayList<Contact> newData) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallback(newData, data));
diffResult.dispatchUpdatesTo(this);
this.data.clear();
this.data.addAll(newData);
}

When we call diffResult.dispatchUpdatesTo(this); it will invoke adapter’s onBindViewHolder(… , List<Object> payloads) method. This is the method where we take care that only those rows should be updated whose data has been changed. Let’s complete the onBindViewHolder(… , List<Object> payloads) method.

TODO (7): complete onBindViewHolder method

Open Adapter/MyRecyclerViewAdapter.java. As stated above we need to update those rows here whose data has been changed. So if the payload is empty then we will call super class’s onBindViewHolder method. If the payload is not empty then we get the bundle from the object list and based on the keys we update the specific views only. So if the name is changed then only the TextView displaying name should be updated not even entire row.

@Override
public void onBindViewHolder(ContactViewHolder 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("name")){
Toast.makeText(holder.itemView.getContext(), "Contact "+position+" : Name Changed", Toast.LENGTH_SHORT).show();;
holder.icon.setText(data.get(position).getName().substring(0,2));
holder.name.setText(data.get(position).getName());
}
if(key.equals("phone")){
Toast.makeText(holder.itemView.getContext(), "Contact "+position+" : Phone Number Changed", Toast.LENGTH_SHORT).show();;
holder.phonenumber.setText(data.get(position).getPhonenumber());
}
}
}
}

You must have a question that if the row’s data hasn’t been changed then the payload will be empty and the super class’s onBindViewHolder() method will be called then how we are stopping the adapter to not update all the views?

The answer is, onBindViewHolder(…, List<Object> payloads) gets called when the RecyclerView is getting populated very first time and only when there is a change in any row. diffResult.dispatchUpdatesTo(this) only invokes onBindViewHolder(…, List<Object> payloads) for those items whose data has been affected.

TODO (8): setOnRefreshListener method to call adapter’s onNewData method

Open MainActivity.java. Now set onRefreshListener() on swipeRefreshLayout as follow. Get the updated data and call adapter’s onNewData() method inside onRefreshListener().

swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
adapter.onNewData(DataGenerator.getUpdatedData());
swipeRefreshLayout.setRefreshing(false);
}
});

That’s all! Now, you can run the app and swipe for refreshing the list and you will see Toast messages showing what specific views were updated inside the RecyclerView.

Mark ❤ if you liked this tutorial. Also, follow us for more tutorials on Android.

--

--