Background data loading
– Android TV application hands on tutorial 15

LoaderManager

Background data loading using Loader class

This Tutorial was explaining about Leanback support library, which is usually used to show list of contents information. So developers may want to load a many meta data to show contents. When Activity or Fragment need to prepare big size of data, it is better to load the data in background.

For this purpose, we can use Loader and LoaderManager. They are useful to implement background data loading followed by updating UI based on these loaded data. I’m going to implement this Loader in this chapter. To study about Loader, I found a very nice article to understand Loader and the LoaderManager in detail (from part 1 to part 4). This post is a basic course, so please refer the above article for advanced understanding.

As a summary of the article, the advantage of using LoaderManager & Loader is

  • Data loading process can be independent from Activity lifecycle.
    • LoaderManager is Activity unique instance (singleton), which handles Loader‘s lifecycle like starting, stopping, reseting. By using Loader, loading process can be independent from Activity lifecycle. One of the most useful situation is when configuration change occurs by display rotation (this is for Android phone and not for Android TV), Activity will be destroyed but Loader‘s information retains.
  • UI thread, background thread handling is easy.
    • This is very similar to AsyncTask so that we can asynchronously prepare data in background thread and get callback to update UI on UI thread. It enables us to develop “good” software architecture easily, so that we can reduce the task of UI thread.
      (See also AsyncTask usage summary.)
  • Loader is Event-driven, data handling is easy & convenient.
    • For example, when data source changed, we can get callback. We can easily update UI according to data change.

To understand implementation we need to take care the relation between Activity/Fragment side and Loader side implementation.

Relationship between Activity, Loader and LoaderManager

LoaderManager is Activity unique instance, and NOT system/application-wise unique instance. Therefore, it is not possible to use a data prepared in Loader A of Activity A from Activity B.

Hands on example: Minimum implementation

Let’s take a look at example of implementing Loader in MainFragment class. We need to implement 2 modules, Loader side which takes care of background data loading process and Activity side with LoaderManager.LoaderCallbacks which handles how to show data on UI.

Activity/Fragment side

1. We will register loader by getLoaderManger().initLoader(id, args, callback) where

  • id: Loader’s id. This should be identical in each Activity, it is ok to use same id number in different Activity because LoaderManager instance exists for each Activity/Fragment.

2. Implementation of LoaderManager.LoaderCallbacks, we need to override 3 methods.

  • onCreateLoader – We will instantiate Loader Here.
  • onLoadFinished – It will called when Loader finish preparing data, data will be shown on UI inside this method.
  • onLoaderReset – If data loading is unsuccessful or stops, this method will be called.

First, let’s implement initLoader in onActivityCreated and register LoaderManager.LoaderCallbacks. Then implement mock of Callbacks as follows.

    private static final int VIDEO_ITEM_LOADER_ID = 1;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        getLoaderManager().initLoader(VIDEO_ITEM_LOADER_ID, null, new MainFragmentLoaderCallbacks());

        ...
    }


    private class MainFragmentLoaderCallbacks implements LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
        @Override
        public Loader<HashMap<String, List<Movie>>> onCreateLoader(int id, Bundle args) {
            /* Create new Loader */
            Log.d(TAG, "VideoItemLoader: onCreateLoader");
            return new VideoItemLoader();
        }

        @Override
        public void onLoadFinished(Loader<HashMap<String, List<Movie>>> loader, HashMap<String, List<Movie>> data) {
            Log.d(TAG, "VideoItemLoader: onLoadFinished");
            /* Loader data has prepared. Start updating UI here */
        }

        @Override
        public void onLoaderReset(Loader<HashMap<String, List<Movie>>> loader) {
            Log.d(TAG, "VideoItemLoader: onLoadReset");
            /* When it is called, Loader data is now unavailable due to some reason. */

        }
    }

return value of onCreateLoader method will be the instance of Loader. Now we are going to implement Loader side. 

Loader side

The Loader is specified as return value of onCreateLoader. I Created new class VideoItemLoader in data package, and extend AsyanTaskLoader<D> to start with.

At least 2 methods need to be implemented (Override).

  • Constructor – parent class, AsyncTaskLoader, requires context. Pass it in the constructor.
  • loadInBackground() – this is just like doInBackground() method of AsyncTask.

What you “must” implement is above two methods. But when I try it, loadInBackground() is never called with this implementation. I needed to implement forceLoad() method in 

  • onStartLoading()

to explicitly start Loading. Below is the code of these three methods implementation.

/**
 * Loader class which prepares Movie class data
 */
public class VideoItemLoader extends AsyncTaskLoader<LinkedHashMap<String, List<Movie>>> {

    private static final String TAG = VideoItemLoader.class.getSimpleName();

    public VideoItemLoader(Context context) {
        super(context);
    }

    @Override
    public LinkedHashMap<String, List<Movie>> loadInBackground() {
        Log.d(TAG, "loadInBackground");

        /*
         * Executed in background thread.
         * Prepare data here, it may take long time (Database access, URL connection, etc).
         * return value is used in onLoadFinished() method in Activity/Fragment's LoaderCallbacks.
         */
        LinkedHashMap<String, List<Movie>> videoLists = prepareData();
        return videoLists;
    }

    @Override
    protected void onStartLoading() {
        //super.onStartLoading();
        forceLoad();
    }

    private LinkedHashMap<String, List<Movie>> prepareData() {
        LinkedHashMap<String, List<Movie>> videoLists = new LinkedHashMap<>();
        List<Movie> videoList = MovieProvider.getMovieItems();
        videoLists.put("category 1", videoList);
        videoLists.put("category 2", videoList);
        videoLists.put("category 3", videoList);
        return videoLists;
    }
}

Data loading process has implemented. Finally, go back to Activity side to handle this data to show on UI.

Go back to Loader side

The last remaining task is to only implementing onLoadFinished callback. The argument is the loaded data coming from Loader

        @Override
        public void onLoadFinished(Loader<LinkedHashMap<String, List<Movie>>> loader, LinkedHashMap<String, List<Movie>> data) {
            Log.d(TAG, "onLoadFinished");
            /* Loader data has prepared. Start updating UI here */
            switch (loader.getId()) {
                case VIDEO_ITEM_LOADER_ID:
                    Log.d(TAG, "VideoLists UI update");

                    /* Hold data reference to use it for recommendation */
                    mItems = new ArrayList<Movie>();

                    mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());

                    int index = 0;
                    /* GridItemPresenter */
                    HeaderItem gridItemPresenterHeader = new HeaderItem(index, "GridItemPresenter");
                    index++;

                    GridItemPresenter mGridPresenter = new GridItemPresenter();
                    ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
                    gridRowAdapter.add(GRID_STRING_ERROR_FRAGMENT);
                    gridRowAdapter.add(GRID_STRING_GUIDED_STEP_FRAGMENT);
                    gridRowAdapter.add(GRID_STRING_RECOMMENDATION);
                    gridRowAdapter.add(GRID_STRING_SPINNER);
                    mRowsAdapter.add(new ListRow(gridItemPresenterHeader, gridRowAdapter));

                    /* CardPresenter */
                    CardPresenter cardPresenter = new CardPresenter();

                    if (null != data) {
                        for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
                            ArrayObjectAdapter cardRowAdapter = new ArrayObjectAdapter(cardPresenter);
                            List<Movie> list = entry.getValue();

                            for (int j = 0; j < list.size(); j++) {
                                Movie movie = list.get(j);
                                cardRowAdapter.add(movie);
                                mItems.add(movie);           // Add movie reference for recommendation purpose.
                            }
                            HeaderItem header = new HeaderItem(index, entry.getKey());
                            index++;
                            mRowsAdapter.add(new ListRow(header, cardRowAdapter));
                        }
                    } else {
                        Log.e(TAG, "An error occurred fetching videos");
                    }
                    /* Set */
                    setAdapter(mRowsAdapter);
            }
        }

Build and run

You can check the source code until here on github, but nothing much changed on UI since loader will do loading process in background. Loader‘s advantage appears when we need a background data loading process which may take long time. As one example of background data loading, I will introduce data loading from the Internet in Next chapter. 

Comments: Loader in another Activity

We have implemented Loader in MainFragment, and how about implementing Loader in another Activity/Fragment, for example VideoDetailsFragment. I thought it is nice if we could use same Loader instance with different Activity. Indeed both MainFragment and VideoDetailsFragment need same VideoList data! However it is very disappointing fact for me that we cannot share the instance of Loader among Activity/Fragment, since LoaderManager is not a system-wise singleton, but the instance (LoaderManagerImpl mLoaderManager) exists in each Activity/Fragment.

Then how should we manage data among between Activity? I think one way is to make an independent data manager class which keeps the instance of data. This is also done in next chapter.

Reference

Leave a Comment

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