AsyncTaskLoader is a subclass of Loader, unlike AsyncTask’s, AsyncTaskLoader prevent duplication of background threads and eliminate duplication of zombie (dead or destroyed) activities.

First, let’s talk about Loaders.

What are Loaders?

The Loader API lets you load data from a content provider or any other data source for display in an FragmentActivity or FragmentDeveloper.Android.com

The concept of Loaders was introduced in Android 3.0 (API Level 11) Honeycomb

Why Use Loaders?

  • Loaders run on separate threads to prevent unresponsive UI.
  • Loaders simplify thread management by providing callback methods when events occur.
  • Loaders persist and cache results across configuration changes to prevent duplicate queries.
  • Loaders can implement an observer to monitor for changes in the underlying data source. For example, CursorLoader automatically registers a ContentObserver to trigger a reload when data changes.

Loading huge data, accessing networkfiledatabase access or complex calculations can be memory intensive and lead to unresponsive UI. A Loader is great choice to load data into an activity asynchronously, efficiently and effectively. There are mainly two types of loaders: AsyncTaskLoader and CursorLoader.

For an AsyncTask-like use case but with more features (such as android lifecycle awareness, preventing the duplication of destroyed activities, running multiple AsyncTaskLoaders simultaneously) AsyncTaskLoader should be used.

If you’re not satisfied with AsyncTaskLoader you can sub-class Loaders directly and define your unique custom implementation.

[box type=”note” align=”” class=”” width=””]AsyncTaskLoader performs the same function as the AsyncTask, but much better[/box]

AsyncTaskLoader

Loaders use the LoaderManager class to manage one or more loaders. AsyncTaskLoader is a type of Loader, thus, LoaderManager  will help us manage it, as well as make it android life-cycle aware.

LoaderManager

LoaderManager includes a set of callbacks (LoaderManager.LoaderCallbacks) for when the loader is created, when it’s done loading data, and when it’s reset. These callbacks include :

  • onCreateLoader() : to instantiate and return a new loader for the given ID.
  • onLoadFinished(): when a previously created loader has finished loading. This is typically the point at which you move the data into activity views.
  • onLoaderReset() : when a previously created loader is being reset, which makes its data unavailable. At this point your app should remove any references it has to the loader’s data.

The above callbacks might not make any sense, keep calm & keep reading, fortune favors the persistent!

Unfortunately, unlike AsyncTask, AsyncTaskLoader does not have onPublishProgressUpdate() to publish progress of background task to the UI. In the example below a simple tweak was done to publish background progress to the UI.

Note : this tweak SHOULD NOT be used in production code because it is not lifecycle aware. There are other ways of publishing results to the UI and also making it lifecycle aware but to keep this tutorial simple and straight to the point, i used a very primitive solution to demonstrate the ideology of publishing results to the UI.

Starting a Loader

Use the LoaderManager class to manage one or more Loader instances within an activity or fragment. Use initLoader() to initialize a loader and make it active. Typically, you do this within the activity’s onCreate() method or onViewCreated() in  a fragment.

How it Works

  1. Admittedly, AsyncTaskLoader is a little bit more complicated than AsyncTask. Please Pay Close ATTENTION!
  2. AsyncTaskLoader is an Abstract class – you must subclass it (just like AsyncTask).
  3. AsyncTaskLoader is a Generic class just like AsyncTask, however, we are required to pass only one parameter variable into the generic angle brackets (<>), which is the type of data we want to return when background processing is done.
  4. AsyncTaskLoader requires you to pass a context into it’s constructor and override loadInBackground()
    class AsyncTaskLoaderSubClass(context: Context): AsyncTaskLoader<Boolean>(context){
     override fun loadInBackground(): Boolean {
     /*This is where background code is executed*/
     }
    }
  5.  The results of loadInBackground() are automatically delivered to the UI thread, by way of the onLoadFinished() LoaderManager callback. forceLoad() must be called before loadInBackground() executes
  6. To start AsyncTaskLoader, in an Activity, call getSupportLoaderManager().initLoader(LOADER_ID, BUNDLE, LoaderManager.LoaderCallbacks<T>) or in a Fragment call getLoaderManager().initLoader(LOADER_ID, BUNDLE, LoaderManager.LoaderCallbacks<T>).
    [box type=”info” align=”” class=”” width=””]For Fragments make sure you import android.support.v4.app.Fragment, android.support.v4.app.LoaderManager.
    Read this and this to know why[/box]
  7. LOADER_ID is the identification number (integer) with which you want to use to identify your AsyncTaskLoader
  8. BUNDLE is Bundle paramenter that you must pass, it gives us an option to send data to our AsyncTaskLoaderSubClass
  9. LoaderManager.LoaderCallbacks<T> is a generic interface which is necessary in order to know when the loader is created, when it’s done loading data, and when it’s reset. “T” represents what (type of data) the background process will return, whatever you define here must be the same as point no. 3.
  10.  Write an Interface class, BackgroundProgressInterface, then, implement it on MainActivity and override the Require methods. as shown below. What is an Interface?.
    [box type=”info” align=”” class=”” width=””]Using an Interface to Update the UI from within AsyncTaskLoader IS NOT IDEAL, because, once the device is rotated or a configuration occurs that trigger activity re-creation, the reference of BackgroundProgressInterface passed into the constructor AsyncTaskLoaderSubClass in step 11 has been bulldozed (cleared off) and it’s impossible to pass a new reference of BackgroundProgressInterface into the constructor of AsyncTaskLoaderSubClass because AsyncTaskLoaders are not recreated when configuration change occurs, hence, it will keep a reference of the old, dead, cleared, defunct BackgroundProgressInterface. The End Result? Our UI will not be Updated.However, there are other legit ways to mimick AsyncTask’s onPublishProgress(). If you’re interested you may read this[/box]

    //The Interface
    interface BackgroundProgressInterface {
        fun onUpdateProgress(progress: Int)
    }
    //MainActivity
    class MainActivity : AppCompatActivity(), BackgroundProgressInterface{
       override fun onUpdateProgress(progress: Int) {
         // our Code will be here
      }
     //Other Codes
    }
  11. Make it Requirement to Pass an Instance of BackgroundProgressInterface into the constructor of AsyncTaskLoaderSubClass. As shown below on line 2.
    class AsyncTaskLoaderSubClass (context: Context, val args: Bundle?,
     val progressInterface: BackgroundProgressInterface) 
    : AsyncTaskLoader<Long>(context) {
    //Other Codes here,but for the sake of brevity!
    }
  12. Within loadInBackground(), you can reference the interface and pass the progress Integer and update the UI as shown below.
    class AsyncTaskLoaderSubClass
        (context: Context, val args: Bundle?, val progressInterface: BackgroundProgressInterface)
            : AsyncTaskLoader(context) {
    
            override fun onStartLoading() {
                super.onStartLoading()
                //onStartLoading() can  be likened to OnPreExecute in AsyncTask
    
                forceLoad() 
                //without calling forceLoad() loadInBackground will not execute
            }
    
            override fun loadInBackground(): Long {
                //other code
                val number = numberString.replace(",", "").toLong()
                var result: Long = 0
                for (i in 1..100) { 
                    //other code
                    progressInterface.onUpdateProgress(i)
                    Log.d(TAG, "Loop $i Result = $result")
                   //other code
                }
                return result
            }
     }
    
    //MainActivity
    class MainActivity : AppCompatActivity(), BackgroundProgressInterface{
       override fun onUpdateProgress(progress: Int) {
         runOnUiThread(Runnable {
                // This will run our code on the UI thread
                kotlin.run {
                    progressTxtView?.setText("Progress = $progress%")
                }
            })
      }
     //Other Code
    }

     

Enough Talk Let’s Code!

App Example

We are going to build an app that does a long complex mathematical computation suitable for background processing using an AsyncTaskLoader, just as we previously did in AsyncTask Example.

We will add 1 to 1,000,000 in a loop of 100. That is, 1 + 2 + … 1,000,000, then repeat the process a hundred times!

[box type=”download” align=”” class=”” width=””]Grab Source Code From GitHub[/box]

OBJECTIVES :

  1. Build an App that add up numbers from 1 to 1 million, in a loop of 100.
  2. Display the progress of the summation

App :

activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    android:layout_margin="16dp"
    android:orientation="vertical">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Begin"
        android:id="@+id/begin_async_task_loader"
        android:textSize="16sp"
        android:layout_marginBottom="16dp"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="22sp"
        android:textStyle="bold"
        android:background="#ef6c00"
        tools:text="Progress = 100 %"
        android:id="@+id/progress"
        android:layout_marginBottom="4dp"
        android:textColor="@android:color/white"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="22sp"
        android:background="#00533e"
        android:id="@+id/final_result"
        android:text="The Result would be posted here"
        android:textColor="@android:color/white"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="cancel"
        android:id="@+id/cancel_async_task_loader"
        android:textSize="16sp"
        android:layout_marginTop="16dp"/>

</LinearLayout>
asynctaskloader app layout

asynctaskloader app layout

MainActivity.java

class MainActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks<Long>
        ,BackgroundProgressInterface {
    val LOADER_ID = 20 // this can be any integer
    val bundleValue: String = "1,000,000" // 1 Million
    var progressTxtView: TextView? = null
    var finalResultTxtView: TextView? = null
    val bundle = Bundle()

    companion object {
        val TAG = "MainActivity"
        val BUNDLE_KEY = "key.to.identify.bundle.value"
    }

    override fun onUpdateProgress(progress: Int) {
        runOnUiThread(Runnable {
            // This will run our code on the UI thread
            kotlin.run {
                progressTxtView?.setText("Progress = $progress%")
            }
        })
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        progressTxtView = findViewById(R.id.progress)
        finalResultTxtView = findViewById(R.id.final_result)
        val beginButton : Button = findViewById(R.id.begin_async_task_loader)
        beginButton.setOnClickListener(View.OnClickListener {
            Log.d(TAG, "Begin Button Tapped!")
            makeOperationAddNumber()
        })
        val cancelButton : Button = findViewById(R.id.cancel_async_task_loader)
        cancelButton.setOnClickListener(View.OnClickListener {
            Log.d(TAG, "Cancel Button Tapped!")
            cancelBackgroundProcess()
        })

        Log.d(TAG, "OnCreate")

        bundle.putString(BUNDLE_KEY, bundleValue)
        supportLoaderManager.initLoader(LOADER_ID, null, this)
    }

    private fun cancelBackgroundProcess() {
        val loader = supportLoaderManager.getLoader<Long>(LOADER_ID)
        if (loader != null) {
            val isCancelled = loader.cancelLoad()
            if (isCancelled) showToastMsg("Loader Canceled!")
        }
    }

    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Long> {
        Log.d(TAG, "Inside onCreateLoader, bundle = $args")
        return AsyncTaskLoaderSubClass(this, args, this)
    }

    override fun onLoadFinished(loader: Loader<Long>?, data: Long?) {
        Log.d(TAG, "Inside onLoadFinished")
        if (data == null || data < 1) {
            Log.d(TAG, "AsyncTaskLoader = cancelled or Bundle = null")
            return
        } else {
            finalResultTxtView?.setText("The Sum of Numbers between 1 to 1 million \n = $data")
            Log.d(TAG, "Final Result = $data")
        }
    }

    override fun onLoaderReset(loader: Loader<Long>?) {}

    private fun makeOperationAddNumber() {
        // this will try to fetch a Loader with ID = LOADER_ID
        val loader: Loader<Long>? = supportLoaderManager.getLoader(LOADER_ID)
        if (loader == null) {
            /* if the Loader with the loaderID not found,
            * Initialize a New Loader with ID = LOADER_ID
            * Pass in a bundle that the AsynTaskLoader will use
            * Also pass the necessary callback which is 'this' because we've implemented it on our activity
            */
            supportLoaderManager.initLoader(LOADER_ID, bundle, this)
        } else {
            /* If the Loader was found with ID = LOADER_ID,
            * Stop whatever it may be doing
            * Restart it
            * Pass in a bundle that the AsynTaskLoader will use
            * Also pass the necessary callback which is 'this' because we've implemented it on our activity
            */
            supportLoaderManager.restartLoader(LOADER_ID, bundle, this)
        }
    }

    fun showToastMsg(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
    }

    class AsyncTaskLoaderSubClass
    (context: Context, val args: Bundle?, val progressInterface: BackgroundProgressInterface)
        : AsyncTaskLoader<Long>(context) {

        override fun onStartLoading() {
            super.onStartLoading()
            // can be likened to OnPreExecute in AsyncTask
            forceLoad() // without calling this loadInBackground will not execute
        }

        override fun loadInBackground(): Long {
            Log.d(TAG, "Inside Load In Background")
            val numberString = args?.getString(BUNDLE_KEY)
            if (numberString == null) {
                Log.d(TAG, "Bundle = null")
                return 0
            }
            val number = numberString.replace(",", "").toLong()
            var result: Long = 0
            for (i in 1..100) { //loop 100
                result = 0
                for (j in 1..number) {
                    result = result + j
                }
                progressInterface.onUpdateProgress(i)
                Log.d(TAG, "Loop $i Result = $result")
                if (isLoadInBackgroundCanceled) {
                    Log.d(TAG, "Complex Background Computation was Canceled")
                    return 0
                }
            }
            return result
        }
    }
}

[box type=”success” align=”” class=”” width=””]Congratulations! for reading this technically boring and quite complicated AsyncTaskLoader.

Stay Awesome, Keep Slaying. Conquer the World![/box]

Leave a comment