Investigating PagedListAdapter Performance and DiffUtil

Siena Aguayo
5 min readSep 16, 2018

--

I’m building a Japanese-English dictionary Android app using the latest Architecture Components (Room, ViewModel, LiveData, Paging, and of course data binding, which I’ve been a fan of for a long time).

Architecture Components overview (courtesy of Android Room with a View)

It’s been really fun so far, but as is the case when working with bleeding edge technology, sometimes you run into problems that StackOverflow hasn’t seen yet, or nobody seems to be talking about. I ran into such a problem with the performance of my RecyclerView that is backed by a PagedListAdapter: paging through a single list worked wonderfully, but when changing the list/query that is backing the adapter, performance would get laggy and I got some unexpected behavior.

First, check out the snappy performance of this RecylerView using PagedListAdapter: my default query returns all ~180,000 rows in the database, and I can fling through that list no problem, loading the default 20 rows at a time.

Flinging through a list of ~180,000 database rows — wow!!

To set this up, my repository returns a LiveData<PagedList<T>

public LiveData<PagedList<SearchResultEntry>> browse() {
return new LivePagedListBuilder<>(entryDao.browse(), 20).build();
}

…and my Dao returns a DataSource.Factory<Integer, T> to get the paged results.

@Query("SELECT id, primary_kanji, primary_reading FROM entries ORDER BY id ASC")
DataSource.Factory<Integer, SearchResultEntry> browse();

I was really impressed with how easy it was to get this part working. I basically just followed the official documentation section on using the Paging library with an on-device database. So cool.

At first, I glossed over the section about the required implementation of DiffUtil.ItemCallback, as I didn’t really get what it was for and figured I would learn more about it when it seemed relevant. The official walkthrough I was following didn’t explain much, just provided a sample implementation:

private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Concert>() {
// Concert details may have changed if reloaded from the database,
// but ID is fixed.
@Override
public boolean areItemsTheSame(Concert oldConcert, Concert
newConcert) {
return oldConcert.getId() == newConcert.getId();
}

@Override
public boolean areContentsTheSame(Concert oldConcert,
Concert newConcert) {
return oldConcert.equals(newConcert);
}
};

As I progressed further with my app’s features, however, I noticed some unique behavior of the PagedListAdapter. If I marked an entry as a “favorite” and that entry was present in the list when I navigated to the Favorites list, the list item would animate:

Keep your eye on the “ditto mark” entry…

This was all well and good, but I also noticed if I changed lists, scrolled down a bunch, then went back to the main list, the app was noticeably lagging and I was occasionally getting the telltale “The application may be doing too much work on its main thread” frames-skipped warning in Logcat. What was maybe even worse than that was that my scroll position seemed to be somewhat unpredictable, as I was not always placed at the top of the list when I changed views.

In the following gif, I begin on Browse, switch to the N5 list, scroll down a bunch, then go back to Browse. I am not at the top of Browse, but rather 3 pages (60 items) into my list.

Note the animation when changing lists via the drawer. When I come back to Browse, I am at the beginning of page 4, not at the top of the list or at my previous scroll position.

This was alarming to me, since aside from the performance issues, it’s a bad user experience. I would expect to be popped back to the top of the list.

I thought perhaps now it was time to go learn about the DiffUtil.ItemCallback I had skipped over earlier. DiffUtil does indeed try to calculate the difference between two lists so it can do some fancy transitions. What I suspect was happening in my previous example was that there could be no reasonable diff calculated between the lists, so the adapter was just putting me scrolled down as far as it thought was sensible, at the top of page 4. I read some library code, and some documentation, and thought about that cool animation from my Favorites screen. That animation is very neat when you’re dealing with smaller, predictable data sets and want to animate between them. But what if you know that the lists you’re switching between aren’t really that similar at all? Is there a way for us to skip performing the diff, even if the diff is supposed to happen on a background thread (which it is)?

I, admittedly, did not look very hard for an option to turn off the diff, but it did make me think that if I re-instantiated the adapter when performing a different kind of search, the adapter would have to start over and therefore wouldn’t perform an unnecessary diff.

Funnily enough, this worked really well: when I change my search type (therefore the kind of query that is backing my RecyclerView), I instantiate a new adapter and assign that to the RecyclerView again.

Changing lists is no problem and I always start at the top!

Could I have solved this a different way? Probably, but this was the simplest thing I could think of. It does seem odd to re-instantiate an adapter. Should I instantiate new fragments every time I change my search type? That also seems like overkill. My ViewModel just needs to change which LiveData query from the repository it is showing, and I just need a way to tell PagedListAdapter not to perform an expensive diff.

How has your experience been with PagedListAdapter?

--

--