How to use AsyncListUtil
A tutorial for the Support Library’s AsyncListUtil, and how to correctly back your RecyclerView with data from a SQLite database.
RecyclerView is great, but when you need to provide hundreds or thousands of items to display using an Adapter you can quickly run out of memory.
Last week, I gave a talk called “Get Creative to squeeze performance from SQLite” at Droidcon Berlin. In it, I suggested an approach to implementing a RecyclerView.Adapter
that’s backed by a cursor. And even though you have to access the database (and thus: the disk) on the UI thread to do so: I imply it’s better to drop a couple of frames occasionally while the cursor fetches data than it is to crash the app (because the alternative was to load too much data into memory).
As it turns out, Riccardo Ciovati was in the audience that day and was kind enough to send me an e-mail after my talk to let me know about the existence of AsyncListUtil
: a utility which makes it possible to back a RecyclerView with a cursor-driven Adapter where the cursor needn’t be accessed on the UI thread.
Mea culpa.
I took Riccardo’s advice and looked into AsyncListUtil
, and at first glance: it’s an ugly API. Let’s try to break it down.
RecyclerView Architecture
This is what we probably all already know and love.
You’ve got a RecyclerView and assign it a LayoutManager
and an Adapter
which provides it with ViewHolder
objects, then updates those objects with data from your app. Currently, you need to be able to answer a few questions and perform a few operations synchronously in order to fulfill the Adapter
's contract. Among those, the most common are to:
- supply the total number of items that the RecyclerView can expect from the Adapter, and
- bind items’ data to
ViewHolder
instances.
RecyclerView + AsyncListUtil Architecture
When you want your RecyclerView to be able to display more data than can fit in memory at once, you should use AsyncListUtil
.
In order to do so, you’ll need to extend two abstract classes: AsyncListUtil.DataCallback
and AsyncListUtil.ViewCallback
, and create an instance of AsyncListUtil
using them. Where you go from there isn’t made clear by the documentation, but I’ve done my best to put together a diagram of how I’ve constructed things in order to make it work:
This chart is the end result of about an hour of massaging and tweaking in order to make it at least kind-of readable. In it, you can see that the Adapter, AsyncListUtil, and ViewCallback communicate in a kind of cycle to update and fill the recycler view.
Also, we’ve added an OnScrollListener that is used to let the AsyncListView know that the viewport has changed (and along with it: the range of items that may come into view soon).
Finally, the DataCallback is used by the AsyncListView to fetch data. And we’ve supplied an ItemSource
to the DataCallback in order to abstract the details of how data is collected.
Maybe it’s better to just look at some code.
Implementing AsyncListUtil
I’ve put together an app that displays a RecyclerView with an Adapter that uses AsyncListUtil.
It’s not really that impressive of a UI, but blog posts like this tend to have an animated gif of what you should expect at the end…
You can find the code on Github and follow along:
The Data
For the purpose of this article, I wrote a short python script which generates an SQLite database file that contains a table called data
with 100,000 records and places it in app/src/main/assets/database.sqlite
. Each record is kind of a dummy blog post or article and has three fields: id, title, and content. The text for title and content is just made up of random words from DWYL’s english-words repository.
The ItemSource
Rather than have the logic used to fetch items from the database be buried within our Adapter code, or be tightly-bound to what should really be a thin DataCallback
extension, I think it’s better to define an interface and use an implementation of that interface within the data callback.
Our interface: ItemSource
declares three methods:
getCount()
— to determine the total number of items available in the source,getItem(position)
— to fetch a particular item at a given position, andclose()
— a method we will call to release any resources the ItemSource is using.
In order to provide Item
objects from SQLite, let’s implement ItemSource
.
In SQLiteItemSource
there are a few things to notice:
- We are defining a
cursor
property that is backed by a variable. This lets us check if the backing variable has been closed (or is null) and re-generate it. - The
getItem(position)
method is implemented by moving thecursor
to the position and instantiating a new Item from the new position. - A private function:
Item(c: Cursor)
behaves like an “extension constructor” and generates a newItem
instance using the cursor.
The Callbacks
In order to create an AsyncListUtil
, we need to pass it a DataCallback
and a ViewCallback
.
Let’s start with the DataCallback
.
We’ve defined our DataCallback
to accept an ItemSource
in its constructor and there are two abstract methods defined by AsyncListUtil.DataCallback
which we’ve implemented:
fillData(data, startPosition, itemCount)
— Called on a background thread byAsyncListUtil
when it decides that it needs more data. It calls theItemSource
'sgetItem
and populatesdata
withitemCount
items, starting from thestartPosition
within the ItemSource.refreshData()
— Also called on a background thread byAsyncListUtil
, but only upon initialization or in response to callingrefresh()
on the AsyncListUtil object itself. It’s responsible for returning the total number of items in the data set. Our implementation simply calls into theItemSource
again.
It’s important to note that we’ve also defined a new method, close
, on our DataCallback and within it: call down into the ItemSource’s close method.
Now for the ViewCallback
:
AsyncListUtil uses the ViewCallback
you pass to its constructor in two main ways:
- To signal the view that data has changed or has been loaded.
- To determine the locations of data being currently displayed by the view, with the purpose of knowing when it’s time to fetch more Items or when it’s okay to free up some memory taken up by old Items not currently within the viewport.
In our implementation, point #1 is accomplished by the onDataRefresh()
and onItemLoaded(position)
methods. They call into the RecyclerView
which was passed to the constructor and signal notifyDataSetChanged()
and notifyItemChanged(position)
respectively.
Determining the current viewport is handled by getItemRangeInto()
. Instead of returning a value, the outRange
parameter needs to be filled with two integers: the positions of the first and last Item
objects that are currently visible in the RecyclerView.
There’s a gotcha here: if the RecyclerView isn’t yet initialized with data, calling the two methods: findFirstVisibleItemPosition
and findLastVisibleItemPosition
on its layout manager will both return -1, but the AsyncListUtil
uses -1 to denote that no data needs to be fetched. So to resolve this, when we see -1 for both positions: we just populate outRange
with zeros instead.
The OnScrollListener: make AsyncListUtil aware of viewport changes
The ScrollListener
's constructor requires the AsyncListUtil
, and in its implementation of onScrollStateChanged
, simply calls the onRangeChanged()
method of the AsyncListUtil.
The ViewHolder
Super straight-forward, our ViewHolder implementation simply updates two TextView
widgets with content from the Item
. Here’s the class:
Note: The item passed to bindView
will be null
when AsyncListUtil is loading the data for it. We’ve handled that situation here by showing “loading” text in both TextViews.
Here’s its layout XML:
The Adapter
Now that all the pieces are in place and ready to be used, we can finally implement our RecyclerView.Adapter
: AsyncAdapter
!
AsyncAdapter’s constructor accepts two parameters: an ItemSource
, and a RecyclerView
. The ItemSource is used to create a DataCallback
, and the RecyclerView goes into creating the ViewCallback
. Next, the listUtil
field is instantiated by creating a new instance of AsyncListUtil
for Item
objects with a 500-item page size and passing it the two callbacks we created. Finally: we create a ScrollListener
using the listUtil
.
AsyncListUtil makes implementing onBindViewHolder(..)
and getItemCount()
a breeze. One thing that’s important to note, however, is that listUtil.getItem(position)
can return null
while the item at the given position is still being loaded. This means you’ll need to handle null
bind values in your ViewHolders similarly to how we did above in ViewHolder.kt.
In addition to the normal RecyclerView Adapter stuff, notice the two methods: onStart(recyclerView)
and onStop(recyclerView)
. They’re hooks we can use within the Activity’s lifecycle methods to add/remove the OnScrollListener
and close the resources held by the ItemSource
within our DataCallback
.
The Activity: Putting it all together
We’re just about done, let’s use our shiny new AsyncAdapter
with a RecyclerView just like we would any other Adapter.
Again, notice the onStart
and onStop
methods, they call into the adapter to let it set-up and tear-down its resources accordingly.
In Conclusion
Without the suggestion from Riccardo, I would’ve never known that AsyncListUtil
existed. It’s been in the Support Library since version 23, and I wasn’t able to find a single tutorial, guide, or article on how to use it.
The API is kind of clunky, but I hope this guide has given you a good feel for how it can fit into your app in situations where you’ve got too much data to keep it all in memory but need to make it viewable within the UI.
Once again, you can find the source code for this tutorial on Github here:
It’s nice to have a way to get the best of both worlds: keeping our heap nice and tidy by loading the data from a database, as well as keeping those database operations off of the UI thread.