[TUT] ASyncTaskLoader using Support Library

I’ve just started learning about Loaders, they’re a way to handle doing tasks off the UI thread. The benefits include having a listener that will inform your fragment/activity when the data your monitoring is changed. They will also account for orientation changes when loading.

asynctaskloader using support library example

A brief introduction is written here and it got me up to speed: An Intro to Loaders

This tutorial has extensive code but is pretty simple in concept. It follows on from the tutorial already written at http://developer.android.com/reference/android/content/AsyncTaskLoader.html. The Google tutorial shows you how to use an ASyncTaskLoader but with the post HoneyComb API. So I’ve taken this code and used it but coded against the support library.

There are two changes and one bug fix that need to be implemented to get this to work! I’ve also increased the OO concepts using correct packaging and class files.

  • Changed out the HC ArrayAdapter and used BaseAdapter
  • Removed the use of the ActionBar search function
  • ASyncTask wont start unless you explicity call forceLoad()!

First off we can’t use the helper methods in extending HC ArrayAdapter, we extend BaseAdapter and do the data management ourselves. In the long run this is probably a better approach for control and customisation.

AppListAdapter.java

package com.blundell.asynctaskloader.ui.adapter;

import java.util.List;

import com.blundell.asynctaskloader.R;
import com.blundell.asynctaskloader.domain.AppEntry;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class AppListAdapter extends BaseAdapter {

	private final LayoutInflater inflater;
	private List<AppEntry> data;

	public AppListAdapter(Context context) {
		inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	public void setData(List<AppEntry> data){
		this.data = data;
		notifyDataSetChanged();
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view;

		if(convertView == null){
			view = inflater.inflate(R.layout.list_item_icon_text, parent, false);
		} else {
			view = convertView;
		}

		AppEntry item = data.get(position);
		((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
		((TextView) view.findViewById(R.id.text)).setText(item.getLabel());

		return view;
	}

	@Override
	public int getCount() {
		return data == null ? 0 : data.size();
	}

	@Override
	public Object getItem(int position) {
		return data == null ? null : data.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

}

I removed the ActionBar search functionality, I don’t think it should be part of this tutorial and just stops you seeing the wood for the trees as they say. So here’s the new fragment.

AppListFragment.java

package com.blundell.asynctaskloader.ui.fragment;

import java.util.List;

import com.blundell.asynctaskloader.domain.AppEntry;
import com.blundell.asynctaskloader.ui.adapter.AppListAdapter;
import com.blundell.asynctaskloader.ui.loader.AppListLoader;

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;

public class AppListFragment extends ListFragment implements LoaderCallbacks<List<AppEntry>> {

	private AppListAdapter adapter;

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);

		setEmptyText("No applications");

		adapter = new AppListAdapter(getActivity());
		setListAdapter(adapter);

		setListShown(false);

		// Prepare the loader
		// either reconnect with an existing one or start a new one
		getLoaderManager().initLoader(0, null, this);
	}

	@Override
	public void onListItemClick(ListView l, View v, int position, long id) {
		Toast.makeText(getActivity(), "Clicked: "+ id, Toast.LENGTH_SHORT).show();
	}

	@Override
	public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
		// This is called when a new loader needs to be created.
		// This sample only has one Loader with no arguments, so it is simple.
		return new AppListLoader(getActivity());
	}

	@Override
	public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
		adapter.setData(data);

		// The list should now be shown
		if(isResumed()){
			setListShown(true);
		} else {
			setListShownNoAnimation(true);
		}
	}

	@Override
	public void onLoaderReset(Loader<List<AppEntry>> loader) {
		// Clear the data in the adapter
		adapter.setData(null);
	}
}

Finally there is a bug when extending ASyncTaskLoader from the support library, the loader doesn’t actually start. It’s written up as an issue here. What you need to do is catch when the loader wants to start loading using onStartLoading(), then query if you have your data yet (because the loader could be restarted) if you have then deliver your data, else force it to start.

package com.blundell.asynctaskloader.ui.loader;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.blundell.asynctaskloader.domain.AppEntry;
import com.blundell.asynctaskloader.receiver.PackageIntentReceiver;
import com.blundell.asynctaskloader.util.Comparator;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.support.v4.content.AsyncTaskLoader;

public class AppListLoader extends AsyncTaskLoader<List<AppEntry>> {

	InterestingConfigChanges lastConfig = new InterestingConfigChanges();
	public final PackageManager pm;

	List<AppEntry> apps;
	PackageIntentReceiver packageObserver;

	public AppListLoader(Context context) {
		super(context);

		// Retrieve the package manager for later use; note we don't
        // use 'context' directly but instead the safe global application
        // context returned by getContext().
		pm = getContext().getPackageManager();
	}


	@Override
	protected void onStartLoading() {
		super.onStartLoading();
		// AsyncTaskLoader doesn't start unless you forceLoad http://code.google.com/p/android/issues/detail?id=14944
		if(apps != null){
			deliverResult(apps);
		}
		if(takeContentChanged() || apps == null){
			forceLoad();
		}
	}


	/**
	 * This is where the bulk of the work. This function is called in a background thread
	 * and should generate a new set of data to be published by the loader.
	 */
	@Override
	public List<AppEntry> loadInBackground() {
		// Retrieve all known applications
		List<ApplicationInfo> apps = pm.getInstalledApplications(
											PackageManager.GET_UNINSTALLED_PACKAGES |
											PackageManager.GET_DISABLED_COMPONENTS);

		if(apps == null){
			apps = new ArrayList<ApplicationInfo>();
		}

		final Context context = getContext();

		// Create corresponding array of entries and load their labels
		List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
		for (ApplicationInfo applicationInfo : apps) {
			AppEntry entry = new AppEntry(this, applicationInfo);
			entry.loadLabel(context);
			entries.add(entry);
		}

		Collections.sort(entries, Comparator.ALPHA_COMPARATOR);

		return entries;
	}

	/**
	 * Called when there is new data to deliver to the client. The super class
	 * will take care of delivering it; the implementation just adds a little more logic
	 */
	@Override
	public void deliverResult(List<AppEntry> apps) {
		if(isReset()){
			// An async query came in while the loader is stopped. We don't need the result
			if(apps != null){
				onReleaseResources(apps);
			}
		}
		List<AppEntry> oldApps = this.apps;
		this.apps = apps;

		if(isStarted()){
			// If the loader is currently started, we can immediately deliver a result
			super.deliverResult(apps);
		}

		// At this point we can release the resources associated with 'oldApps' if needed;
		// now that the new result is delivered we know that it is no longer in use
		if(oldApps != null){
			onReleaseResources(oldApps);
		}
	}

	@Override
	protected void onStopLoading() {
		// Attempts to cancel the current load task if possible
		cancelLoad();
	}

	@Override
	public void onCanceled(List<AppEntry> apps) {
		super.onCanceled(apps);

		// At this point we can release the resources associated with 'apps' if needed
		onReleaseResources(apps);
	}

	/**
	 * Handles request to completely reset the loader
	 */
	@Override
	protected void onReset() {
		super.onReset();

		// ensure the loader is stopped
		onStopLoading();

		// At this point we can release the resources associated with 'apps' if needed
		if(apps != null){
			onReleaseResources(apps);
			apps = null;
		}

		// Stop monitoring for changes
		if(packageObserver != null){
			getContext().unregisterReceiver(packageObserver);
			packageObserver =  null;
		}
	}

	/**
	 * Helper function to take care of releasing resources associated with an actively loaded data set
	 */
	private void onReleaseResources(List<AppEntry> apps){
		// For a simple list there is nothing to do
		// but for a Cursor we would close it here
	}

	public static class InterestingConfigChanges {
		final Configuration lastConfiguration = new Configuration();
		int lastDensity;

		protected boolean applyNewConfig(Resources res){
			int configChanges = lastConfiguration.updateFrom(res.getConfiguration());
			boolean densityChanged = lastDensity != res.getDisplayMetrics().densityDpi;
			if(densityChanged || (configChanges & (ActivityInfo.CONFIG_LOCALE|ActivityInfo.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0){
				lastDensity = res.getDisplayMetrics().densityDpi;
				return true;
			}
			return false;
		}
	}
}

That should do it, if you run up your app you’ll see a list of all the applications running on your device. I also changed the fragment to be instantiated in XML instead of with the fragment manager. This was only done in the Google example because it’s part of a bigger package of examples.

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <fragment
        android:id="@+id/fragment_app_list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        class="com.blundell.asynctaskloader.ui.fragment.AppListFragment" />

</RelativeLayout>

For the full source code see below:

Download link for the eclipse project source

GitHub Repo Mirror


3 thoughts on “[TUT] ASyncTaskLoader using Support Library

  1. Pretty good tutorial. There’s something that’s bugging me, perhaps more than it should and that is why do we do this:

    @Override
    public void deliverResult(List apps) {
    if(isReset()){
    // An async query came in while the loader is stopped. We don’t need the result
    if(apps != null){
    onReleaseResources(apps);
    }
    }

    }

    Why would the Loader be in a reset status? 😕 I mean, it IS deliveringResults, right? as the method suggests. My question is, when does a Loader enters the reset status, why, and why is it that we don’t show data when we’re in it.

    1. Reset means stopped, so imagine you’re activity asks to load some list but whilst it’s hitting the database the user navigates elsewhere, the loader is no longer needed and therefore the result is not needed either

Leave a Reply

Your email address will not be published. Required fields are marked *