Android AsyncTaskLoader

The Android SDK has evolved quite a bit since I last worked on Cabbie Pro. Most notably, the release of Ice Cream Sandwich, aka Android 4.0, brought with it a whole new set of design & interaction patterns with associated development building blocks.

I decided to write a simple app to get acquainted with these changes in the Android platform. The app I chose to experiment with is quite simple: it would display a bus schedule. A simple list of timings showing when a bus is scheduled to depart with just one twist, for departure times in the upcoming thirty minutes, the list would switch from showing something like 'at 10:45' to 'in 10 minutes'. The schedule itself is static and is stored locally, it is just the display which needs to be dynamic.

One new piece of infrastructure in the Android SDK is the LoaderManager and associated Loader classes. These are designed to help manage the loading & display of data while taking care of maintaining & cleaning up state along with the activity lifecycle. The SDK documentation explains these in more detail.

For the purpose of this app, using the SDK supplied AsyncTaskLoader made the most sense. It uses a background thread to load data and then supply that to the UI thread for rendering, typically in a ListView. The API docs for this class supply a complete example of how to use it. Pretty much the only thing I need to do on top of that example is to periodically refresh the displayed list.

Suppose each scheduled timing is modelled as a Timing object and we populate a ListView with instances of these - one per row - using an ArrayAdapter. Since the adapter would call the toString() method of Timing objects to populate the list view, one dumb way to refresh the list would be to compute the display string each time in the toString() method. It is dumb because (a) it blocks the UI thread making the UI janky, (b) doesn't actually update the display unless the row is scrolled off and back on the screen.

The approach I took is to spawn off a thread in the AsyncTaskLoader which would wake up periodically and call forceLoad() causing the loader to deliver new data to the LoaderManager and subsequently the ArrayAdapter.

Here are my onStartLoading() and onStopLoading() methods of TimingsLoader, a class that extends AsyncTaskLoader:

@Override
protected void onStartLoading() {

    isAppRunning = true;

    if (refreshThread != null) {

        refreshThread.interrupt();

    } else {

        refreshThread = new Thread() {
            public void run() {

                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(30);
                    } catch (InterruptedException e) {
                    }

                    synchronized (refreshLock) {
                        while (!isAppRunning) {
                            try {
                                refreshLock.wait();
                            } catch (InterruptedException e) {
                            }
                        }
                    }

                    TimingsLoader.this.mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            TimingsLoader.this.forceLoad();
                        }
                    });
                }
            };
        };

        refreshThread.start();
        forceLoad();

    }
}

@Override
protected void onStopLoading() {
    super.onStopLoading();
    isAppRunning = false;
}

The forceLoad() calls the loadInBackground() method (not shown here) which would compute the list of new data and deliver it back to the UI.

While the thirty second sleep & subsequent forceload() call takes care of periodically refreshing the list view, there are a couple of scenarios it doesn't account for:

  • When the user navigates away from the app by hitting the home button or via a notification to another app, the background thread continues to run causing unnecessary wake-ups and battery drain.
  • When the user locks and unlocks the screen, while the app is still in the foreground, the list doesn't immediately refresh after unlocking. It waits for the next 30 second refresh cycle. This isn't ideal.

Both these problems can be solved by tracking when the app is visible and when it is backgrounded. I do this by calling the stopLoading() and startLoading() methods of the TimingsLoader instance from the onPause() and onResume() method of the main application activity. I use a volatile boolean isAppRunning variable to capture these state changes.

Finally, I use a refreshLock monitor object to sleep until interrupted when the app is backgrounded. An interrupt is delivered to the refresh thread from the onStartLoading() method so that it starts computing new results as soon as the app is resumed.

There is actually an isStarted() method in the base Loader class that can be used to track (start/stop)Loading states. But I can't use the same here because the underlying state variable used there is non-volatile. This works out okay for isStarted() because it is only meant to be called from the main thread but not for the present use-case where the state needs to be updated from one thread and read from another - hence the use of a volatile isAppRunning variable.

Another point to note is the 'mHandler' variable which is bound to an instance of Handler class in the TimingsLoader's constructor. Since the constructor is called from the main thread, the handler gets bound to the main thread too. Thus, calling post() on this handler causes the provided runnable to be executed on the main thread. Why go through all this trouble? Because, as documented, a Loader's forceLoad() must be called from the main thread!

In conclusion, the new LoaderManager framework provides a clean and convenient mechanism to keep expensive computation off the main thread. I am happy with how this app turned out and plan to use this new framework feature in Cabbie Pro too!

Comments

You should make your content more visual, there is too much text.


Markdown formatting supported