One of my favorite parts of RxJava is that no matter how complex a state management usecase is, there’s always an operator for that™. During a recent code cleanup, I found a very cool usecase of RxJava’s scan()
and compose()
operators for simplifying the usually verbose DiffUtil implementation.
I had been delaying writing this post for a while, but Mark Allison’s new series of articles on adding item change animations to RecyclerView have finally pushed me to finish this.
For those unaware, DiffUtil exists because maths is difficult. It’s a utility class for RecyclerView
that calculates diffs between adapter data-set updates and accordingly plays item change animations by calling methods like notifyItemInserted()
, notifyItemMoved()
, etc. on the adapter.
The usage of DiffUtil normally looks like like this:
List<T> lastItems;
database.streamItems()
.doAfterNext(nextItems -> lastItems = nextItems)
.subscribe(nextItems -> {
recyclerViewAdapter.updateItems(nextItems);
DiffResult result = DiffUtil.calculate(ItemDiffer.create(lastItems, nextItems));
result.dispatchUpdatesTo(recyclerViewAdapter);
});
For usecases where both the previous item and the current item in a stream are required for comparison, RxJava offers a handy operator called scan()
:
database.streamItems()
.scan(seedPair, (oldPair, nextItems) -> {
DiffUtil.Callback callback = ItemDiffer.create(pair.items, nextItems);
DiffResult result = DiffUtil.calculate(callback, true);
return Pair.create(nextItems, result);
})
.skip(1) // seedPair is fake news.
.subscribe(pair -> {
recyclerViewAdapter.updateItems(pair.items);
pair.diffResult.dispatchUpdatesTo(recyclerViewAdapter);
})
scan()
can look really overwhelming at first, especially because its documentation and marble diagram only start making sense after one has already understood what the operator does.
Nevermind the diagram, let’s break-down its usage,
scan(seedPair, (oldPair, nextItems) -> {
// do comparison.
return pair;
})
- seedPair is the initial value to
scan()
because it needs atleast two values in the stream for doing a comparison. The dummy seed value is immediately ignored by callingskip(1)
. oldPair
is the previous value produced by thescan()
function.nextItems
is the new value consumed by the function.
You have probably already noticed that our Rx now chain looks way more verbose with scan()
than before. This isn’t what you signed up for. We will simplify the chain in two steps:
1. Make the adapter responsible for consuming new items
class RecyclerViewAdapter implements Consumer<Pair<List<>, DiffResult> {
void accept(Pair<List<>, DiffResult> pair) {
this.items = pair.items;
pair.diffResult.dispatchUpdatesTo(this);
}
}
This simplifies the subscribe call:
database.streamItems()
.scan(seedPair, (oldPair, nextItems) -> {
DiffUtil.Callback callback = ItemDiffer.create(pair.items, nextItems);
DiffUtil.DiffResult result = DiffUtil.calculate(callback, true);
return Pair.create(nextItems, result);
})
.skip(1)
.subscribe(recyclerViewAdapter)
2. Extract the diffing logic as a reusable function
Considering that real world applications usually have multiple RecyclerViews that need item change animations, Observable#compose()
can be leveraged to make the usage of DiffUtil
reusable:
public class RxDiffUtil {
public static <T> ObservableTransformer<List<T>, Pair<List<T>, DiffResult>> calculate(
BiFunction<List<T>, List<T>, DiffUtil.Callback> itemDiffer)
{
Pair<List<T>, DiffUtil.DiffResult> seedPair = Pair.create(Collections.emptyList(), null);
return upstream -> upstream
.scan(seedPair, (oldPair, nextItems) -> {
DiffUtil.Callback callback = itemDiffer.apply(oldPair.items, nextItems);
DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback, true);
return Pair.create(nextItems, result);
})
.skip(1); // downstream shouldn't receive seedPair.
}
}
The method signature is probably unreadable because of generics and type information, but all we have done is wrap scan()
and skip(1)
with an ObservableTransformer
so that they can be used elsewhere.
DiffUtil can now be dropped anywhere with just one line:
database.streamItems()
.compose(RxDiffUtil.calculate((oldItems, newItems) -> ItemDiffer.create(oldItems, newItems)))
.subscribe(recyclerViewAdapter)
We aren’t done yet. The compose line can be further simplified by using a method reference:
database.streamItems()
.compose(RxDiffUtil.calculate(ItemDiffer::create)
.subscribe(recyclerViewAdapter)
And that’s it.
The source code for this article can be found here: https://github.com/Saketme/RxDiffUtil