AndroidPub
Published in

AndroidPub

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.

Photo by Ahsan Avi on Unsplash

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).

A slide from my talk. I still agree with this in principle, but as it turns out: for RecyclerView adapters it’s just plain wrong.

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:

Hmm…

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…

Not bad.

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, and
  • close() — 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 the cursor 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 new Item 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 by AsyncListUtil when it decides that it needs more data. It calls the ItemSource's getItem and populates data with itemCount items, starting from the startPosition within the ItemSource.
  • refreshData() — Also called on a background thread by AsyncListUtil, but only upon initialization or in response to calling refresh() on the AsyncListUtil object itself. It’s responsible for returning the total number of items in the data set. Our implementation simply calls into the ItemSource 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:

  1. To signal the view that data has changed or has been loaded.
  2. 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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jason Feinstein

Jason Feinstein

Software Engineer at Google. You can find my stuff on GitHub at https://github.com/jasonwyatt