Simple, Functional Async RecyclerView Diffing

A List undergoing change from State A to B

TJ Dahunsi

Feb 26 2019 · 6 mins

Categories:
android
kotlin

Android’s DiffUtil and its DiffResult output have received lots of coverage, and all warn about how diff calculation is expensive and shouldn’t be done on the UI thread. Most examples go ahead to calculate the diff on the UI thread anyway, and for very good reason: The RecyclerView is very tetchy about modifications to it’s state from a non UI thread.

The persnickety nature of the RecyclerView has been detailed exhaustively by Jon Hancock here. I highly recommend reading that article as it’s where I found my feet. Further reading should include Erik Hellman’s “A nice combination of RxJava and DiffUtil”. With the information in those articles digested, we can start to think of a generic one shot approach to tackling diffing in RecyclerViews regardless of List or RecyclerView Adapter type.

Let’s start with some code. The star of the show is a simple interface, Differentiable:

public interface Differentiable { String getId(); default boolean areContentsTheSame(Differentiable other) { return getId().equals(other.getId()); } default Object getChangePayload(Differentiable other) { return null; } }

Differentiable Interface

That is, in order to be able to be recognized as distinct, an item need only provide a unique String identifier. All other callbacks are optional, lending to a semi functional interface. Coverage on them can be seen in the Android docs of [DiffUtil.Callback](https://developer.android.com/reference/android/support/v7/util/DiffUtil.Callback.html).

Next, let’s look at what the DiffCallback for Differentiable items would look like:

class DiffCallback<T> extends DiffUtil.Callback { private final List<T> stale; private final List<T> updated; private final Function<T, Differentiable> diffFunction; DiffCallback(List<T> stale, List<T> updated, Function<T, Differentiable> diffFunction) { this.stale = stale; this.updated = updated; this.diffFunction = diffFunction; } @Override public int getOldListSize() {return stale.size();} @Override public int getNewListSize() {return updated.size();} @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return apply(oldItemPosition, newItemPosition, (stale, current) -> stale.getId().equals(current.getId())); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return apply(oldItemPosition, newItemPosition, Differentiable::areContentsTheSame); } @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) { return apply(oldItemPosition, newItemPosition, Differentiable::getChangePayload); } private <S> S apply(int oldPosition, int newPosition, BiFunction<Differentiable, Differentiable, S> function) { return function.apply(diffFunction.apply(stale.get(oldPosition)), diffFunction.apply(updated.get(newPosition))); } }

A generic DiffUtilCallback that can diff any List provided there’s a way to map type T to a Differentiable

Finally, utilization of the above 2 to get a DiffResult:

/** * Calculates the {@link androidx.recyclerview.widget.DiffUtil.DiffResult} between the source * list and the output of the combiner function applied to the source list and the additions. * </p> * Regardless of implementation, the combiner function is pure, it does not mutate the source * list or the additions list, and as such is best used for diff calculations * in a background thread. * * @param src The original source list in the {@link androidx.recyclerview.widget.RecyclerView} * @param additions Input to the combiner function to mutate the source list * @param combiner A function that yields a new list for which the diff will be calculated * @param diffingFunction A function that determines how items in the lists are differentiated * from each other * @param <T> Type parameter of the lists * @return A POJO containing the result of the diff, along with the output of the combiner * function */ @NonNull public static <T> Diff<T> calculate( List<T> src, List<T> additions, BiFunction<List<T>, List<T>, List<T>> combiner, Function<T, Differentiable> diffingFunction) { List<T> updated = combiner.apply(new ArrayList<>(src), new ArrayList<>(additions)); DiffUtil.DiffResult result = calculateDiff(new DiffCallback<>(new ArrayList<>(src), updated, diffingFunction)); return new Diff<>(result, updated); }

A simple method for calculating the diff between any list, on any thread

Let’s talk about that last bit some more and how it works. The first 2 arguments are Lists of any type T. The first is the original List reflected by the current state of the RecyclerView Adapter. The second List could be any non-null List:

  1. It could be the new state the RecyclerView should reflect.

  2. It could be a List of items to add to the existing list, perhaps the RecyclerView endless scrolls and there’s new data to add.

  3. It could be a List to filter the current List on.

… and so on. All that matters is that it’s a List of the same type T.

The third argument, the combiner, is the real implementation detail. It’s what combines the source List, and the additions, subsequently returning a new List that represents the new state of the RecyclerView. The 3 lines of code in the method body show that regardless of the implementation of the combiner function, the entirety of the operation is pure; there are no side effects. The operands in the combiner function are copies of the source and addition Lists. Moreover, the DiffCallback makes yet another copy of the source so if the source were to change at some point later, the DiffResult is a perfect snapshot of the two states the RecyclerView has transitioned between should the result of the diff calculation be applied to the RecyclerView.

The last argument, diffingFunction, is a function that maps the type T to a Differentiable. As mentioned earlier, the easiest implementation is to return a unique String id for each item. Provided the hashCode() method is properly implemented for T, using

item ->; (Differentiable) String.valueOf(item.hashCode())

would suffice.

The result of the method, a Diff<T>, contains the DiffResult, and the new List that would be accurately reflected via the application of said DiffResult to the RecyclerView, i.e a non generic Pair. At this point nothing has really happened. The RecyclerView’s Adapter hasn’t been notified, neither has the original List changed, and that’s perfect because this calculation can be run on a background thread with no side effects whatsoever. As said before, the implementation is a pure function.

public class Diff<T> { public final DiffUtil.DiffResult result; public final List<T> cumulative; private Diff(DiffUtil.DiffResult result, List<T> cumulative) { this.result = result; this.cumulative = cumulative; } }

The Diff class

So how would one use this then? Consider a list of dancing tiles that change every 2 seconds. Using RxJava:

private void dance() { disposables.add(Flowable.interval(2, TimeUnit.SECONDS, Schedulers.io()) .map(__ -> makeNewTiles()) .map(newTiles -> Diff.calculate(tiles, newTiles, (tilesCopy, newTilesCopy) -> newTilesCopy)) .observeOn(mainThread()) .subscribe(diff -> { tiles.clear(); tiles.addAll(diff.cumulative); processor.onNext(diff.result); }, processor::onError)); }

Diffing with dancing tiles

The makeNewTiles() method is invoked every 2 seconds on a background thread, at which point a new tile Diff is calculated. On the main thread, the List backing a RecyclerView is updated, and the DiffResult is pushed to the View which will eventually call DiffResult[.dispatchUpdatesTo](https://developer.android.com/reference/android/support/v7/util/DiffUtil.DiffResult.html#dispatchUpdatesTo(android.support.v7.widget.RecyclerView.Adapter))(adapter). No IndexOutOfBoundsException; inconsistency detected error to worry about.

Result of the dance method. Result of the dance method

For endless scrolling, the code looks like this:

public Single<DiffUtil.DiffResult> getMoreTiles() { return Single.fromCallable(() -> Diff.calculate(originalTiles, generateTiles(), (origialTilesCopy, generatedTilesCopy) -> { origialTilesCopy.addAll(generatedTilesCopy); return origialTilesCopy; })) .subscribeOn(io()) .observeOn(mainThread()) .map(diff -> { originalTiles.clear(); originalTiles.addAll(diff.cumulative); return diff.result; }); }

Single from callable waits till the Single is subscribed to, before generating new tiles and calculating the diff. Since the subscription is on a background thread, the diffing will also be done there. The combiner function simply adds the new items generated to the copy of the old ones then returns them. Back on the UI thread, the original list swaps its contents out for the result of the combiner and posts the DiffResult to the hosting View.

Endless Scrolling with Diffing Endless Scrolling with Diffing

If you read Jon’s post, both these examples follow the queueing approach, as all diffing requests are handled by RxJava which queues them.

Coroutines would work just as well however. Wrap Diff.calculate in a suspending function, and apply the the result when you’re back on the main thread, async diffing made simple.

The one caveat is the space complexity of diffing. There are 2 copies made of the original List, and 1 copy of the additions. A small caveat for simplicity IMO. More importantly, this code lends itself to a plethora of use cases. I’ve used it for diffing endless scrolling lists, lists representing scanning for Bluetooth devices, to real time search results. Checkout the sample app below for a bunch of examples; including both described above. The endless scrolling list particularly, has been scrolled to 10,000 items using this diffing implementation with no issues.

Note: The AndroidX libraries does provide the ListAdapter class to handle diffing for you internally, however, that requires delegating asynchronicity to the View layer which makes me feel a bit uneasy. I prefer to keep my View classes as simple as possible. Let the Repository and ViewModel classes handle the concurrency in the data layer. The View should just be notified when it ought to change.

That’s all there is to it, reusable code to diff any RecyclerView Adapter backed by a List of any type T. You don’t have to create a different DiffCallback for a different adapter. You don’t even need to make your model objects implement Differentiable, heck you could provide a different diffFunction to diff 2 separate instances of the same type of Adapter. It’s a simple functional approach to diffing, and there’s a lot to like.

,