Since the introduction of Loaders in Honeycomb and Compatibility Library, the Android world has changed for the better. However, it took me quite some time to dissect and understand the rather scarce documentation about how to work with these on special cases, like the hated case of screen configuration change – aka screen flips.

It turns out it’s a pretty straightforward concept – of course, that after you go through a lot of trials and errors, read all the available documentation including the one from the compatibility library’s source code etc…

I’m going to illustrate it below in a simple, but very common case. Let’s suppose I have a button that once pressed, starts fetching some data in a background thread, using an AsyncTaskLoader. Meanwhile, to make my little example more useful, I am displaying a progress dialog with a simple “Loading…” message on top of my activity. If I flip the screen and the background thread still has work to do, I pretty much expect to see the loading dialog “on the other side” (after the screen configuration change). I also expect that my loader is still busy doing the work in background until it’s done, like nothing has happened.

The LoaderManager class has two methods:

public abstract <D> Loader<D> initLoader(int id, Bundle args,
            LoaderManager.LoaderCallbacks<D> callback);

and

public abstract <D> Loader<D> restartLoader(int id, Bundle args,
            LoaderManager.LoaderCallbacks<D> callback);

The first is supposed to start a new loader with the given id or to re-connect with an existing loader with that id. The second one is supposed to start a new loader with the give id OR – and this is different from the initLoader()re-start an existing loader with that id.

This means, in the case of my AsyncTaskLoader, that if I call restartLoader(), my loader will execute the background work again. If I call initLoader() it WILL NOT execute the background data again, but if that loader has finished its job, it will immediately return the last valid result.

Let’s take a look at the sample below.

@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		// this is really important in order to save the state across screen
		// configuration changes for example
		setRetainInstance(true);

		// LoaderManager.enableDebugLogging(true);

		mRes = getResources();
		mInflater = LayoutInflater.from(getActivity());

		mBtnReload = (Button) getActivity().findViewById(R.id.btn);
		mBtnReload.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				// first time
				if (mFirstRun) {

					mFirstRun = false;
					mBtnReload.setText(mRes.getString(R.string.restart));

					startLoading();
				}
				// already started once
				else {
					restartLoading();
				}
			}
		});

		// you only need to instantiate these the first time your fragment is
		// created; then, the method above will do the rest
		if (mAdapter == null) {
			mItems = new ArrayList<String>();
			mAdapter = new MyAdapter(getActivity(), mItems);
		}
		getListView().setAdapter(mAdapter);

		// ---- magic lines starting here -----
		// call this to re-connect with an existing
		// loader (after screen configuration changes for e.g!)
		LoaderManager lm = getLoaderManager();
		if (lm.getLoader(0) != null) {
			lm.initLoader(0, null, this);
		}
		// ----- end magic lines -----

		if (!mFirstRun) {
			mBtnReload.setText(mRes.getString(R.string.restart));
		}
	}

	protected void startLoading() {
		showDialog();

		// first time we call this loader, so we need to create a new one
		getLoaderManager().initLoader(0, null, this);
	}

	protected void restartLoading() {
		showDialog();

		mItems.clear();
		mAdapter.notifyDataSetChanged();
		getListView().invalidateViews();

		// --------- the other magic lines ----------
		// call restart because we want the background work to be executed
		// again
		Log.d(TAG, "restartLoading(): re-starting loader");
		getLoaderManager().restartLoader(0, null, this);
		// --------- end the other magic lines --------
	}

	@Override
	public Loader<Void> onCreateLoader(int id, Bundle args) {
		AsyncTaskLoader<Void> loader = new AsyncTaskLoader<Void>(getActivity()) {

			@Override
			public Void loadInBackground() {
				try {
					// simulate some time consuming operation going on in the
					// background
					Log.d(TAG, "loadInBackground(): doing some work....");
					for (int i = 0; i < SIZE; ++i) {
						mItems.add(WORDS[i]);
					}

					Thread.sleep(SLEEP);
				} catch (InterruptedException e) {
				}
				return null;
			}
		};
		// somehow the AsyncTaskLoader doesn't want to start its job without
		// calling this method
		loader.forceLoad();
		return loader;
	}

	@Override
	public void onLoadFinished(Loader<Void> loader, Void result) {

		mAdapter.notifyDataSetChanged();
		hideDialog();
		Log.d(TAG, "onLoadFinished(): done loading!");

	}

If I comment the magic lines (44 – 47) and I flip the phone to change the layout, from portrait to landscape or vice-versa, my loader state WILL BE LOST (oh no!) and I will never receive the onLoadFinish() callback call to tell me that the job is done. Since I display a loading dialog while the data is being processed in the background, this means that this dialog it will “hang” on the screen forever and ever without being hidden. That is because we need to re-connect - aka initLoader() - with all our current loaders in order to receive feedback after a screen configuration change or whatever made that fragment be restored.

Also, if I replace restartLoader() in the other magic line (line 73) with initLoader(), the second time I press the “Restart” button it WILL NOT do the background work again (meaning to re-add all the items in the list).  So, nothing will happen and the list will remain empty.  If that were a CursorLoader instead, if a previous load would have been completed with valid results, those results will be returned immediately.

That being said, I’m attaching the sources if you want to try out for yourself. Enjoy! :)

FragmentWithDialog.tar