Contents
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
isActivity
unique instance (singleton), which handlesLoader
‘s lifecycle like starting, stopping, reseting. By usingLoader
, loading process can be independent fromActivity
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 butLoader
‘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.)
- This is very similar to
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 becauseLoaderManager
instance exists for eachActivity/Fragment
.
2. Implementation of LoaderManager.LoaderCallbacks
, we need to override 3 methods.
onCreateLoader
– We will instantiateLoader
Here.onLoadFinished
– It will called whenLoader
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 likedoInBackground()
method ofAsyncTask
.
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