A Block Loading ListView BaseAdapter for Android

Over the last couple of months I have been having fun building drop-in Android social components for the Singly Android SDK. One component we created is a Friend Picker ListView that supports any number of friends. Believe it or not there are people out there who have friend lists with > than 15,000 contacts.

The requirements were that performance should be the same if the user has 10 or 10,000 contacts. To achieve that we created a block loading ListView adapter. That is a fancy way of saying a BaseAdapter that supports any number of rows in the list while having good memory usage.

In this post I am going to detail out how to create block loading ListView adapter. All of this code is open source and can be found in the com.singly.android.component package of the Singly Android SDK.

Requirements

We are going to create a custom BaseAdapter class. This class will need to be able to support lists of any size. If a person has 15,000 contacts, it should still work and it shouldn’t break Android. Scrolling should always be smooth no matter where we are in the list. We also want to be able to reuse this adapter for multiple listings and different row types.

Block Loading

The basic algorithm we will use is similar to a database, block loading and caching. Imagine you have a list of 100 rows and you want each block to contain 10 rows. You would have 10 total blocks in the list. To support low memory devices you only want to keep the 5 most used blocks in memory at any time. Blocks are loaded upon use if not already cached and they are removed on an LRU basis depending on how often the rows in the block are accessed.

When a user scrolls to a certain row the adapter will check to see if the block containing that row is loaded. If it is loading the row will be returned. If it isn’t loaded, the adapter will return null and proceed to load the block in the background. How a block is retrieved is implementation dependent, for example you may get rows from a web service. How blocks are cached and rows retrieved from blocks can be made reusable.

Blocks will go through three stages: not loaded, loading, and loaded. Blocks will be cached via an LruCache with statistics being updated anytime a row is accessed. We can limit the total number of blocks we cache as well as the total number of rows per block.

Here is a shell for our BaseAdapter class.

We setup variables for the number of rows in the list, number of blocks, number of rows per block (blockSize). We can also configure memory usage by limiting the number of blocks that are cached at any time.

Notice we have our LruCache to hold the blocks and then synchronized sets for different stages of block loading. The synchronized sets allow block loading to progress in stages and prevents reloading of blocks that are already in the process of being loaded.

Here is our constructor. Notice the rows parameter. You will need to now the total number of rows that the ListView will support. We don’t load all rows at once but we do need to know the total number of rows to be able to determine the number of total blocks.

Notice we precalculate the total number of blocks and set the maxBlockId. This is to bound row requests later.

A block is lazily loaded when a row is requested and the block isn’t already cached. This usually happens from the getView method. For example.

The main method that does the block loading is a bit bigger.

I need to go off on a tangent here as it relates to this method. I wanted to make scrolling as fast as possible. This means avoiding any unecessary calculations in the loadBlocks method as it is called on every row. To this end we only try and load new blocks if we have moved halfway through a given block in any direction. This is what the lastCheckpoint variable is for, tracking distance travelled through a block.

In another post I will show how to add a table of contents to a ListView. If that were present a user could jump to a position inside the list and then begin scrolling forwards or backwards. We want that scrolling experience to be as smooth as possible. To support this when a given row is requested we will try and load the block for the current row as well as N number of blocks before and after the current block. This way, unless the user is scrolling very fast, they should never see rows loading.

In the code above we get the current, previous and next block ids. The number of blocks to load before and after is configurable. We also bound blocks. If a previous or next block doesn’t exist a -1 blockId is put into the blocksToLoad array and then ignored when we do the acutally block loading.

We finish off by loading the blocks. More specifically we make sure that the block isn’t already loading and then we set the block id into the loading set and obtain a load flag. We do this in a synchronized way to prevent blocks from loading more than once. Then with the load flag we call the loadBlock method to load an individual block.

As we said before, loading an individual block is implementation dependent. The load block method below which allows subclasses to retrieve the actual rows however they see fit as long as rows can be retrieved with an offset and limit.

Subclasses must call the finishAndCacheBlock method within loadBlock to finish the caching process, remove loading flags and add loaded flags to the synchronized sets. This also calls the adapter notifyDataSetChanged() method to redisplay rows on the ListView. A user will scroll to a row in a block that hasn’t been loaded, the block will be loaded in the background and when finished the finishAndCacheBlock is called which displays the newly loaded rows in the ListView.

And this completes the block loading process. Now lets take a look at retrieving an individual row.

The getBackingObject returns the single row object from the block from the cache. If the block with the row is not loaded or is loading then null is returned. Usually the ListView would display a loading notification within the row telling the user that the row is loading when this occurs.

The getView BaseAdapter method we saw earlier is left for subclasses to implement. Normally this would involve getting the row object using the getBackingObject method and then displaying the row layout however is desired.

The full code to the AbstractCachingBlockLoadedListAdapter can be found here.

Implementing the loadBlock Method

Here is an example of how you might implement the loadBlock method. We call an api, parse the resulting JSON, create an object for each row and then return the rows as a List.

In Action

If you want to see a demonstration of the adapter and ListView in action you can download the Singly Android SDK and build the examples app. There is a Friends example that loads all of your friends across multiple social networks.

6 thoughts on “A Block Loading ListView BaseAdapter for Android

  1. Hi Dennis,

    I am using singly loading/caching mechanism to my project. Thanks for making it opens.
    What change do I require to make this loading block-by-block rather than pre-defined set of rows. The way facebook android app does.

    Singly needs this because user wants to jump to friends name and want to skip others. But I want user to see it sequentially. So basically one block load and cache itself and when user flings-up we will show loading and will load next block. It would be great if you can show me some direction how singly can be used for this.

    regards,

    1. Not sure if I understand what you are asking. The current code does load block-by-block, which works great unless you want to do an endless scrolling type of view where you don’t know the total number of rows in advance.

      When the Singly code jumps to a friends name either through scroll or by letter press on the table of contents it jumps to a position. The list view requests that position and underneath that the code loads the block that holds that row. All the other rows in that block are also loaded. In the Singly code there is a web service backing that that loads x rows with a given offset and limit, essentially a block.

      1. Dennis,

        I am using loadBlock() method for loading block by block and calling it on OnScroll(). On very first scroll after downloading first 20 rows onScroll() gives me position 19 and as per getBlockIdandPos() method it is blockId 0 which is true. I am not able to show next block which is get downloaded due to this. I have removed loadBlocks(). Any suggestion would be great help.

        1. The loadBlocks() method is what loads x number of previous and next blocks from a current position. It is run for every row but only loads the next block when a trigger position is reached, usually half the block but it is configurable.

          I wouldn’t expect it to work if the loadBlocks method was removed. You may want to check out its functionality if you are trying to do something similar. Usually you will be loading more than one block at a time to enable smooth forwards and backwards scrolling. You will also want to preload the blocks so that on scrolling the next block is already loaded once they reach the block start.

          1. Thanks Dennis. I have converted it to single block loading at a time. Which was already there I have just modified it little bit to suite my requirement. Instead of downloading single image of friend per row I am downloading 5 images per row hence created 5 ImageInfo object and called listener on them. But surprisingly it downloads only first 5 images.

            Any idea of doing it better way. Essentially I want multiple images to be downloaded for single row.

Comments are closed.