Android 3.0 - what are the advantages of using LoaderManager instances exactly?

Zsombor Erdődy-Nagy picture Zsombor Erdődy-Nagy · Apr 9, 2011 · Viewed 8.3k times · Source

With 3.0 we got the fancy LoaderManager, which handles data loading using the AsyncTaskLoader, the CursorLoader, and other custom Loader instances. But reading through the docs for these I just couldn't get the point: how are these better than just using the good old AsyncTask for data loading?

Answer

hackbod picture hackbod · Apr 9, 2011

Well they are a lot simpler to implement, and take care of everything about lifecycle management so are much less error prone.

Just look at the sample code, for showing the result of a cursor query which lets the user interactively filter the result set through a query input field in the action bar:

public static class CursorLoaderListFragment extends ListFragment
        implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {

    // This is the Adapter being used to display the list's data.
    SimpleCursorAdapter mAdapter;

    // If non-null, this is the current filter the user has provided.
    String mCurFilter;

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

        // Give some text to display if there is no data.  In a real
        // application this would come from a resource.
        setEmptyText("No phone numbers");

        // We have a menu item to show in action bar.
        setHasOptionsMenu(true);

        // Create an empty adapter we will use to display the loaded data.
        mAdapter = new SimpleCursorAdapter(getActivity(),
                android.R.layout.simple_list_item_2, null,
                new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS },
                new int[] { android.R.id.text1, android.R.id.text2 }, 0);
        setListAdapter(mAdapter);

        // Prepare the loader.  Either re-connect with an existing one,
        // or start a new one.
        getLoaderManager().initLoader(0, null, this);
    }

    @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        // Place an action bar item for searching.
        MenuItem item = menu.add("Search");
        item.setIcon(android.R.drawable.ic_menu_search);
        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
        SearchView sv = new SearchView(getActivity());
        sv.setOnQueryTextListener(this);
        item.setActionView(sv);
    }

    public boolean onQueryTextChange(String newText) {
        // Called when the action bar search text has changed.  Update
        // the search filter, and restart the loader to do a new query
        // with this filter.
        mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
        getLoaderManager().restartLoader(0, null, this);
        return true;
    }

    @Override public boolean onQueryTextSubmit(String query) {
        // Don't care about this.
        return true;
    }

    @Override public void onListItemClick(ListView l, View v, int position, long id) {
        // Insert desired behavior here.
        Log.i("FragmentComplexList", "Item clicked: " + id);
    }

    // These are the Contacts rows that we will retrieve.
    static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
        Contacts._ID,
        Contacts.DISPLAY_NAME,
        Contacts.CONTACT_STATUS,
        Contacts.CONTACT_PRESENCE,
        Contacts.PHOTO_ID,
        Contacts.LOOKUP_KEY,
    };

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // This is called when a new Loader needs to be created.  This
        // sample only has one Loader, so we don't care about the ID.
        // First, pick the base URI to use depending on whether we are
        // currently filtering.
        Uri baseUri;
        if (mCurFilter != null) {
            baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                    Uri.encode(mCurFilter));
        } else {
            baseUri = Contacts.CONTENT_URI;
        }

        // Now create and return a CursorLoader that will take care of
        // creating a Cursor for the data being displayed.
        String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
                + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
                + Contacts.DISPLAY_NAME + " != '' ))";
        return new CursorLoader(getActivity(), baseUri,
                CONTACTS_SUMMARY_PROJECTION, select, null,
                Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
    }

    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)
        mAdapter.swapCursor(data);
    }

    public void onLoaderReset(Loader<Cursor> loader) {
        // This is called when the last Cursor provided to onLoadFinished()
        // above is about to be closed.  We need to make sure we are no
        // longer using it.
        mAdapter.swapCursor(null);
    }
}

Correctly implementing this full example yourself with AsyncTask is going to involve a lot more code... and even then, are you going to implement something as complete and well working? For example, will your implementation retain the loaded Cursor across activity configuration changes so it doesn't need to be re-queried when the new instances is created? LoaderManager/Loader will do that automatically for you, as well as taking care of correctly creating and closing the Cursor based on the activity lifecycle.

Also notice that using this code doesn't require that you think at all about making sure long running work is performed off the main UI thread. LoaderManager and CursorLoader take care of all of that for you, ensuring you will never block the main thread while interacting with the cursor. To do this correctly you actually need to have two Cursor objects active at the same time at points, so you can continue to display an interactive UI with your current Cursor while the next one to show is being loaded. LoaderManager does all of that for you.

This is just a much simpler API -- no need to know about AsyncTask and think about what needs to run in the background, no need to think about activity lifecycle or how to use the old "managed cursor" APIs in Activity (which didn't work as well as LoaderManager anyway).

(Btw don't forget the new "support" static library that let you use the full LoaderManager API on older versions of Android down to 1.6!)