Friday, February 24, 2012

Loading, Loading, Loading...

Keep that train a' Loading...

With the Introduction of Android 3.0 Honeycomb, came a lot of new features, but one I really have found useful is the implementation of Loaders. Loaders provide the basis for loading data in the background, weather it is from an SQLite Database, Images from a webservice or just a shit-load of items into a list a Loader's are the way to go!

While only a mere 4.4% of Android devices are on 3.0 or higher (as of Feb 1st 2012), Loaders were added to the Android Compatibility Library provides support all the way back to 1.6 (as does Jake Whorton's Action Bar Sherlock library). That's right, support all the way back to DONUT! (Yum) Not only are they super useful, but they are super easy to use. Here's how to go about it:
First you need to add the compatibility library (or ABS) to your project if you're going to be using anything less than Android 3.0. Next, you need to build your loader. While the Loader class is the abstract class, it does not inherently provide multi-thread support for asynchronous loading. It's best to either use a CursorLoader or extend from AsyncTaskLoader. Obviously if you're loading data from a database or ContentProvider then a CursorLoader is what you're looking for. Most cases, however, you'll want to implement your own Loader class that extends from AsyncTaskLoader. There are a few things to make sure you watch out for when implementing your custom loader:

  1. While loading your data, check to see if the Loader was cancelled. The easiest way to do this is to add a boolean object to your Loader that you can check, and set the appropriate value in your Loader's onCancelled() method or cancelLoad() method. The cancelLoad() method attempts to call cancel() on the built in AsyncTask, but it's nice to tell your own load that it's time to stop.
  2. You must initiate the load from somewhere in your Loader. A great place to do this is in the onStartLoading() method, and all you have to do is call forceLoad().
  3. Check if you already have your data loaded when a request to onStartLoading() is called, and you can save time and possible data by just delivering the result you already loaded once.
  4. Clear your cached data in onReset() and to release any resources you can (closing cursors and the sort) since this means someone wants fresh data.
That's pretty much it for "make-sure-to-dos". Here is an example of an AsyncTaskLoader implementation:

import android.content.AsyncTaskLoader;
// Import android.support.v4.content.AsyncTaskLoader if using the compatability library

public class MyAsyncLoader extends AsyncTaskLoader {
    // MyDataType can be anything (lookup Java Generics) including Arrays or JSONObects
   private MyDataType mData; // This is essentially our "cache"
   private boolean mCancelled = false; //So we can check if we need to stop loading, not always required.
// We have to have a constructor that calls super(Context)
   public MyAsyncLoader(Context ctx) {
      super(ctx);
   }

   @Override
   public void onCancelled() {
      // Attempt to cancel our asynctask
      cancelLoad();
      mCancelled = true;
     // Don't need any data
      mData = null;
  }

  // This get's called after a loader is initialized or a loader that is alive still is reset 
  @Override
  public void onStartLoading() {
      mCancelled = false;
      if( mData != null) { // Have our data loaded, just deliver it!
          deliverResult(mData);
          return;
      }
      forceLoad(); // This starts the AsyncTask that will load our data.
 }

  //This is called when an Activity or Fragment requests a loader to be reset because they want new data 
  @Override
   public void onReset() {
      mData = null;
      cancelLoad(); // Ensure that the old task is cancelled if it was running
      // We do NOT have to call forceLoad here because onStartLoading will get called after this
   }
 
   // Here we just want to store our own data we got and reset our boolean
   @Override
   public void deliverResult(MyDataType data) {
      mData = data;
      mCancel = false; // I'm pretty sure this is redundant, but I have it here anyway
     if( isStarted()) {
         // Only want to deliver the result if the loader wasn't stopped or cancelled. The super class can do the rest
         super.deliverResult(mData);
     }
   }

   // Now the last part!! 
  @Override
   public MyDataType loadInBackground() {
          // Essentially this method is called from the built in AsyncTask of the parent class
       // Load our junk, and if possible check if mCancelled is true and abort loading if it is 
       return loadedData;
  }
} // end of MyAsyncLoader

Now that we have our Loader defined, we need to implement it in our Activity (or Fragment. To do this, we use the LoaderManager and LoaderManager.LoaderCallBacks. You can implement the callbacks on the Activity itself, using a seperate class or as an anonymous inner class. The 3 callbacks you need to use are:

  1. onCreateLoader()
  2. onLoadFinished()
  3. onLoaderReset()
It's best to set a progress bar or something in your onCreateLoader and onLoaderReset methods and then clear it in onLoadFinished(), which I've done in the example below. Then you use the LoaderManager to start and reset your Loader. Here's an example:
import 
android.app.Activity; //if using the support library use android.support.v4.app.FragmentActivity
import android.app.LoaderManager.LoaderCallbacks; // if using support lib use android.support.v4.app.LoaderManager.LoaderCallbacks

public class MyLoaderActivity extends Activity
        implements LoaderManager.LoaderCallbacks {
   private MyDataType mLoadedData;

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(Bundle savedInstanceState);
      // We're going to use the built in title bar/action bar progress indicator
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
      setContentView(R.layout.mylayout);

      // Use the LoaderManager to load our data in the background
      // If using the support library call getSupportLoaderManager() instead
      getLoaderManager.initLoader(0, null, this);
      // Arguments are (ID of the loader, optional Bundle of arguments, Callbacks)
   }

   //Loader Callbacks
   public Loader onCreateLoader(int id, Bundle args) {
       // Since we only have one type of data and loader, we don't care about the id
      // We also don't use any arguments (you can create other constructors to take arguments into your loader (URLs, data types, etc)
      // We do want to start our progress bar though
      setProgressBarIndeterminateVisibility(true);
      return new MyAsyncLoader(this);
   }


   public void onLoaderReset(Loader loader) {
      // Again, start the progress bar
      setProgressBarIndeterminateVisibility(true);
      mLoadedData = null;
      // Don't really care about the loader since we know what it was, everything else is already called by the framework. This would be a great place to clear ListAdapters or whatnot if you don't want old data visible.
   }


   public void onLoadFinished(MyDataType data) {
      //Turn off our progress bar
      setProgressBarIndeterminateVisibility(false);
      mLoadedData = data;
      // Do whatever else you need to here, like setting a ListAdapter, etc 
   }


} // End of MyLoaderActivity

That's all there is to it! Seems like a bit more than I had initially thought, but it's still pretty simple. Go and load to your hearts content!

No comments:

Post a Comment