Are you Struggling With Loaders Implementation in Android?

Nyame Bismark
5 min readOct 30, 2020

In android we have a lot of ways of performing execution of task in the background or asynchronously. If you were with me in my last article, we talked about how we could use AsyncTask to execute task asynchronously.

If you missed this, then you can click on the linked above to have a look at it. Today, we are going to have a look at how Loaders are implemented in android with a valid example to give a full understanding. Please save me exactly 6 mins of your precious time to sail through this journey together.

Loader is a class that perform asynchronous loading of data and while they are active, they should monitor the source of the data and when there is a content change, new results are delivered. A client of a loader should as a rule perform all the calls onto a loader on the main thread and the work is executed by a loader, AsyncTaskLoader a direct subclass of Loader, on the worker thread and the result should be delivered to the client on the main thread.

Any subclass of a Loader class should have the following methods implemented:

  1. onStartLoading() : This part is automatically called by the loader when the activities or the fragments are started. You should not bother to call this method as it may conflict with the management of the loader. When the data is ready then a callback will be passed to the main thread.
  2. onStopLoading() : This has to be implemented by the subclass as it will be called the the loader through stopLoading (this is not called directly from the client )and this will be called from the process main thread.
  3. onReset() : This also has to be implemented by the subclass and it will be called as a result of reset (this is not called directly from the client ) and it will be done on the main thread too.

We have other interesting method hooks you can take a look at even though we may not need all during implementation but it is cool to get the understanding like onForceLoad(). One caveat is if you want use the context, kindly use it through the getContext as the loaders will be called in a lot of activities, so it will be dangerous to store the context. Take a look at the android official documentation page for more. 😊

Enough of the blabbering, let us take a deep dive into how we can implement Loaders in our applications. We will be using the asyncTaskLoader to load all application installed on a device as our example.

To start with, let us see how Loaders are crafted before diving into the implementation.

Here we are using fragment and it will implement Loader.LoaderCallbacks. We need to implement these:

a. onCreateLoader
b. onLoadFinished
c. onLoaderReset

/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class LoaderFragment : ListFragment(), LoaderManager.LoaderCallbacks<AppListLoader.AppsDataPair> {

val ID_LOADER_APP_LIST = 0
lateinit var adapter: AppsAdapter

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val lv = listView
LoaderManager.getInstance(this).initLoader(ID_LOADER_APP_LIST, null, this)
adapter = AppsAdapter(requireContext(),R.layout.rowlayout)
lv.adapter = this.adapter;
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AppListLoader.AppsDataPair> {

return AppListLoader(requireContext())
}

override fun onLoadFinished(
loader: Loader<AppListLoader.AppsDataPair>,
data: AppListLoader.AppsDataPair?
) {
// set new data to adapter
adapter.setData(data!!.first)

if (isResumed) {
setListShown(true)
} else {
setListShownNoAnimation(true)
}

}

override fun onLoaderReset(loader: Loader<AppListLoader.AppsDataPair>) {
adapter.setData(null)

}
}

Now we can take a look at the implementation of the AsyncTaskLoader class.
1. We look at loadInBackground method.

@Nullable
@Override
public AppsDataPair loadInBackground() {
//retrieve all the installed apps on the device
List<ApplicationInfo> apps = packageManager.getInstalledApplications(
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);

//Check if the there isn't any installed apps on the device
if (apps == null) return new AppsDataPair(Collections.emptyList(), Collections.emptyList());
mApps = new AppsDataPair(new ArrayList<>(apps.size()), new ArrayList<>(apps.size()));

//Retrieve the info of individual apps on the device
for (ApplicationInfo applicationInfo : apps) {
File sourceDir = new File(applicationInfo.sourceDir);

//get the name of the application installed
String label = applicationInfo.loadLabel(packageManager).toString();
Drawable icon = applicationInfo.loadIcon(packageManager);
PackageInfo info;

try {
info = packageManager.getPackageInfo(applicationInfo.packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
info = null;
}

AppData elem =
new AppData(
label == null ? applicationInfo.packageName : label,
applicationInfo.sourceDir,
applicationInfo.packageName,
applicationInfo.flags + "_" + (info != null ? info.versionName : ""),
Formatter.formatFileSize(getContext(), sourceDir.length()),
sourceDir.length(),
sourceDir.lastModified(),
icon);

mApps.first.add(elem);

Collections.sort(mApps.first, AppData.ALPHA_COMPARATOR);

for (AppData p : mApps.first) {
mApps.second.add(p.path);
}
}

return mApps;
}

When the task is loaded in the background, it will be passed to the deliverResult method hook.

@Override
public void deliverResult(@Nullable AppsDataPair data) {
if (isReset()) {

if (data != null) onReleaseResources(data);
}

// preserving old data for it to be closed
AppsDataPair oldData = mApps;
mApps = data;
if (isStarted()) {
// loader has been started, if we have data, return immediately
super.deliverResult(mApps);
}

// releasing older resources as we don't need them now
if (oldData != null) {
onReleaseResources(oldData);
}
}

When the activity has started, the onStartLoading will be called.

@Override
protected void onStartLoading() {
if (mApps != null) {
// we already have the results, load immediately
deliverResult(mApps);
}

if (packageReceiver == null) {
packageReceiver = new PackageReceiver(this);
}

boolean didConfigChange = InterestingConfigChange.isConfigChanged(getContext().getResources());

if (takeContentChanged() || mApps == null || didConfigChange) {
forceLoad();
}
}

When the activity has stopped, a call will be made to onStopLoading.

/**
* Handles a request to stop the Loader.
*/
@Override
protected void onStopLoading() {
cancelLoad();
}

When the activity is destroyed, then a call will be made to onReset.

/**
* Handles a request to completely reset the Loader.
*/
@Override
protected void onReset() {
// we're free to clear resources
if (mApps != null) {
onReleaseResources(mApps);
mApps = null;
}

if (packageReceiver != null) {
getContext().unregisterReceiver(packageReceiver);

packageReceiver = null;
}

InterestingConfigChange.recycle();
}
This is the output after the implementation

This is just the skeleton of the work and if you want the full project or the work, then you may take a look at this for full implementation.

Thank you for staying together with me for this journey and if you like this article, then like it and share. 👌 nyamebismark12.nb@gmail.com for more questions and clarifications.

References
https://developer.android.com/reference/android/content/Loader

--

--