Advanced Use of DiffUtil with Kotlin and RxJava 2

If you have a list in your app that needs to be updated from time to time, you have to use the DiffUtil class. DiffUtil is a utility class that calculates the difference between two lists and updates only items that have changed. To do this correctly, the DiffUtil class uses the notifyItem* methods of the RecycleView.Adapter. The DiffUtil class also has a callback that allows it to calculate changes to any two lists, not only in RecycleView:

public interface ListUpdateCallback {
    
    void onInserted(int position, int count);

    void onRemoved(int position, int count);

    void onMoved(int fromPosition, int toPosition);
  
    void onChanged(int position, int count, Object payload);
}

Calculations can take some time, so it’s best to use a separate thread to prevent blocking of the UI. When I faced this problem, I found a lot of articles with examples of only basic use of the DiffUtil class to calculate how a whole item has changed. But the DiffUtil class can also update just parts of an item. I decided to write this article to show you how to update only a specific part of an item asynchronously.

How I created my sample project

To show my vision for how to overcome the challenge of UI blocking, I created a sample project. It’s a simple app that contains one screen with a list of timezones and the current time in those timezones. Each list item shows a timezone name along with hours, minutes, and seconds.

advanced-use-of-diffutil-sample

Every second, the app gets the current time from different instances of the Calendar class and updates only those TextViews that have changed.

I chose Kotlin for development since it allowed me to create the app faster and safer than Java. For me, writing with Kotlin’s modern syntax is a pleasure. I also chose RxJava 2 – Reactive Extensions of the JVM (Java Virtual Machine) – to deal with asynchronous events. RxJava 2 offers a convenient interface for comfortable interaction with asynchronous events. The dependencies of build.gradle are as follows:

dependencies {
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile "io.reactivex.rxjava2:rxjava:2.1.0"
    compile 'com.android.support:design:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}

The app loads an array of timezone IDs from its string resources, forms a list of Calendars based on IDs, and puts the result in the Flowable class:

val timeZonesIds = resources.getStringArray(R.array.timezones_ids).asIterable()

        val flowable = Flowable.fromIterable(timeZonesIds)
                .map { getInstance(TimeZone.getTimeZone(it)) }
                .map { Time(it.timeZone.id, it.get(HOUR_OF_DAY), it.get(MINUTE), it.get(SECOND)) }
                .toList()
                .repeatWhen { it.delay(1, TimeUnit.SECONDS) }
                .subscribeOn(Schedulers.computation())

The project contains a base class for RecyclerView.Adapter that encapsulates its default logic:

abstract class BaseAdapter<D, VH : BaseViewHolder<D>> : RecyclerView.Adapter<VH>() {

    var dataSource: List<D> = emptyList()

    override fun getItemCount() = dataSource.size

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): VH {
        val inflater = LayoutInflater.from(parent?.context)
        val view = inflater.inflate(getItemViewId(), parent, false)
        return instantiateViewHolder(view)
    }

    abstract fun getItemViewId() : Int

    abstract fun instantiateViewHolder(view: View?): VH

    override fun onBindViewHolder(holder: VH, position: Int) {
        holder.onBind(getItem(position))
    }

    fun getItem(position: Int) = dataSource[position]
}

Now we can inherit from the base class to implement the usual adapter with a minimum of code. Another benefit of this approach is the ability to focus on the main logic of the DiffUtil class.

DiffUtil.Callback is an abstract class:

 public abstract static class Callback {
   
        public abstract int getOldListSize();
   
        public abstract int getNewListSize();

        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
 }

The functions of the getOldListSize() and getNewListSize() methods speak for themselves. Usually, areItemsTheSame() gets old and new items, compares their IDs, and returns a result:

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
        = oldList[oldItemPosition].id == newList[newItemPosition].id

override fun getOldListSize(): Int = oldList.size

override fun getNewListSize(): Int = newList.size

The method areContentsTheSame() compares data from list items:

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val new = newList[newItemPosition]
        val old = oldList[oldItemPosition]
        val isHoursTheSame = new.h == old.h
        val isMinutesTheSame = new.m == old.m
        val isSecondsTheSame = new.s == old.s
        return isHoursTheSame && isMinutesTheSame && isSecondsTheSame
}

If areItemsTheSame() returns true and areContentsTheSame() returns false, the DiffUtil class invokes getChangePayload(). This method defines which content has changed. You can implement it like this:

companion object {
        const val HOURS = "HOURS"
        const val MINUTES = "MINUTES"
        const val SECONDS = "SECONDS"
}

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val new = newList[newItemPosition]
        val old = oldList[oldItemPosition]
        val set = mutableSetOf<String>()
        val isHoursTheSame = new.h == old.h
        val isMinutesTheSame = new.m == old.m
        val isSecondsTheSame = new.s == old.s
        if(isHoursTheSame.not()) {
            set.add(HOURS)
        }
        if(isMinutesTheSame.not()) {
            set.add(MINUTES)
        }
        if(isSecondsTheSame.not()) {
            set.add(SECONDS)
        }
        return set
}

You can choose to output the result as any data type. The schematic below explains how the DiffUtil class compares lists.

advanced-use-of-diffutil-shema
To handle the difference between old and new item content, you have to override the onBindViewHolder(VH holder, int position, List<Object> payloads) method. This method is invoked after calling notifyItemChanged(int, Object) or notifyItemRangeChanged(int, int, Object):

 override fun onBindViewHolder(holder: TimeViewHolder?, position: Int, payloads: MutableList<Any>?) {
        if(payloads?.isEmpty() ?: true) {
            super.onBindViewHolder(holder, position, payloads)
        } else {
            val set = payloads?.firstOrNull() as Set<String>?
            set?.forEach {
                when(it) {
                    TimeDiffCallback.HOURS -> {
                        holder?.tvHours?.setTime(getItem(position).h)
                    }
                    TimeDiffCallback.MINUTES -> {
                        holder?.tvMinutes?.setTime(getItem(position).m)
                    }
                    TimeDiffCallback.SECONDS -> {
                        holder?.tvSeconds?.setTime(getItem(position).s)
                    }
                    else -> super.onBindViewHolder(holder, position, payloads)
                }
            }
        }
}

Payloads contains information you can use to update only one part of an item. If payloads is empty, you just need to call the super method and it will invoke onBindViewHolder(VH holder, int position). This method contains the logic to perform simple data binding to an instance of ViewHolder. onBindViewHolder(VH holder, int position) is invoked only if you call super on onBindViewHolder(VH holder, int position, List<Object> payloads).

The setDataSource() method of the adapter takes an instance of the Flowable class, uses the DiffUtil class to calculate the difference between old and new lists, and returns the Disposable class to deal with the Activity lifecycle:

fun setDataSource(flowable: Flowable<List<Time>>) : Disposable {
        var newList: List<Time> = emptyList()
        return flowable
                .doOnNext { newList = it }
                .map { DiffUtil.calculateDiff(TimeDiffCallback(dataSource, it)) }
                .observeOn(AndroidSchedulers.mainThread())
                .doOnNext { dataSource = newList }
                .subscribe { it.dispatchUpdatesTo(this) }
}

To synchronously highlight dividers, I used ValueAnimator and UpdateListener, since doing so results in a lot less code:

companion object {
        @JvmStatic
        val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
            this.repeatCount = ValueAnimator.INFINITE
            this.repeatMode = ValueAnimator.REVERSE
            this.duration = 400
            this.start()
        }
}
......
  class TimeViewHolder(itemView: View?) : BaseViewHolder<Time>(itemView) {

        val vFirstDivider by lazy { itemView?.findViewById(R.id.vFirstDivider) }
        val vSecondDivider by lazy { itemView?.findViewById(R.id.vSecondDivider) }

        init {
            valueAnimator.addUpdateListener {
                vFirstDivider?.alpha = it.animatedFraction
                vSecondDivider?.alpha = it.animatedFraction
            }
        }
......
    

The DiffUtil class is a useful tool that allows you to use RecyclerView simply and correctly. It’s easy to use and it handles all logic of complex calculations. Using DiffUtil, you can prevent many bugs and make your code clearer. You can also make DiffUtil asynchronous using, for instance, RxJava or the default Thread class. Our sample project processes large amounts of data, and DiffUtil deals with this data pretty quickly.

4.6/ 5.0
Article rating
14
Reviews
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Would you tell us how you feel about this article?