Exploring asynchronous data loading with Loaders in Android Android 17.08.2017

Exploring asynchronous data loading with Loaders in Android

Android introduced Loaders in 3.0 to ease asynchronous tasks on Activity and Fragments. It monitor the source of the data and automatically deliver new result when content changes. It's primary usage is to work with a database utilizing the CursorLoader, but it can also be used to do other asynchronous tasks. I want to take a look at how Loaders could be used as a replacement for AsyncTask.

The issue with AsyncTask is well known. If you rotate the device, you have to cancel the AsyncTask or the app will crash. Loaders handles rotation better, it automatically reconnect to the last loader’s cursor when being recreated after a configuration change avoiding the need to re-query their data.

So, the API of Loaders is designed to deal with two issues with loading data by activities and fragments.

The first is the non-deterministic nature of activities where an activity can be hidden partially or fully, restarted due to device rotation, or removed from memory when in background due to low-memory conditions. These events are called activity life cycle events. Any code that retrieves data must work in harmony with the activity life cycle events. Prior to the introduction of Loaders, this was handled through Managed Cursors. This mechanism is now discontinued in favor of Loaders.

The second issue with loading data in activities and fragments is that data access could take longer on the main thread resulting in application-not-responding (ANR) messages. Loaders solve this by doing the work on a worker thread and providing callbacks to the activities and fragments to respond to the asynchronous nature of data fetch.

Some of the characteristics or features offered by Loaders are:

  • They interact with the data source in a background thread to provider asynchronous loading of data. Once data is available, the loader triggers a callback (generally in the UI thread) with the data that can be used to modify the user interface for instance.
  • They’re available to every Activity and Fragment and plays well with their respective lifecycles. This means if the Activity or Fragment is stopped (or destroyed), the loaders will also stop.
  • Loaders can monitor (observe) the data source to deliver/update new results when the underlying data/content changes.
  • On a configuration change, for eg. an orientation change, loaders that were running in the background continue to do their work. Then they retain their state over a configuration change so that they don’t have to re-query their data and delivers their cached data to the recreated Activity. They operate on Application context, hence the Activity Context object undergoing a configuration change is not preserved causing memory leak.

Loader API involves following classes

  • LoaderManager
  • LoaderManager.LoaderCallbacks
  • Loader
  • AsyncTaskLoader
  • CursorLoader

LoaderManager. Every Activity uses a single LoaderManager object to manage the loaders associated with that activity. A LoaderManager can have multiple loaders but there’s only one LoaderManager per client. LoaderManager is like a mediator between a client and its loaders. To get a LoaderManager object, you don’t instantiate it but call Activity.getLoaderManager() or Fragment.getLoaderManager().

Once a loader is registered with a loader manager, the LoaderManager will facilitate the necessary callbacks to

  • create and initialize the Loader
  • read the data when the Loader finishes loading the data
  • close the resource when the loader is about to be destroyed as the activity is no longer needed.

The LoaderManager is hidden from you and you work with it through callbacks and LoaderManager public APIs. The creation of the LoaderManager is controlled by the activity. LoaderManager is almost like an integral part of the Activity itself.

It is the responsibility of the registered Loader to work with its data source and also with the LoaderManager to read the data and send the results back to the LoaderManager. The LoaderManager will then invoke the callbacks on the activity that data is ready. The Loader is also responsible for pausing the data access or monitoring data changes or working with the LoaderManager to understand and react to the activity life cycle events.

While you can write a loader from scratch for your specific data needs by extending the Loader API, you typically use the Loaders that are already implemented in the SDK. Most loaders extend the AsyncTaskLoader which provides the basic ability to do its work on a worker thread freeing the main thread. When the worker thread returns data, the LoaderManager will invoke the main callbacks to the activity that the data is ready on the main thread.

There are two important methods that the LoaderManager API exposes: initLoader(), restartLoader(). These two methods basically creates new loaders to use and has certain differences.

LoaderManager.LoaderCallbacks. For the clients to interact with the LoaderManager to create loaders and handle the data loaded by the loaders, they need to implement the LoaderManager.LoaderCallbacks interface.

LoaderCallbacks is where everything actually happens. And by "everything", I mean three callbacks:

  • onCreateLoader() - here’s where you construct the actual Loader instance
  • onLoadFinished() - this is where the results you deliver appear
  • onLoaderReset() - your chance to clean up any references to the now reset Loader data

Loader. The Loader framework has a Loader class that basically represents a loader performing asynchronous loading of data. This class is not very interesting as you wouldn’t use it nor derive another class from it really. Instead it has a subclass called AsyncTaskLoader which uses an AsyncTask to perform the data loading work in a background thread. It relies on AsyncTask.executeOnExecutor() to perform its background execution. This is the class that should be extended to write your own custom loaders.

A loader can be in four different states that is held (instance variables) by Loader.

  • Reset. In this state, the loader gives up any data associated with it for garbage collection.
  • Started. This is the started state achieved by a call to startLoading() that’ll invoke the onStartLoading() callback that you must override where you shall start loading the data by making a call to forceLoad() that invokes the onForceLoad() callback on the AsyncTaskLoader that in turn will call loadInBackground() on a worker thread where you do your background loading operations.
  • Stopped. In this data, no data can be delivered to the client (that can only happen in the Started state). It may observe/monitor for changes and load content in the background for the purpose of caching that can be used later if the loader is started again. From this state the loader can be started or reset.
  • Abandoned. An intermediary state between stopped and reset where it holds the data until a new loader is connected to the data source, so that the data is available until the restart is completed.

CursorLoader. The Loader framework has the CursorLoader class that subclasses AsyncTaskLoader. This class can be used to load Cursor objects from ContentProvider data sources only. It’s actually a loader with the Cursor data type (extends AsyncTaskLoader<Cursor>). CursorLoader itself manages the lifecycle of the Cursor object too, so for example no need to close it in your application code. Note: It cannot be used with SQLite databases, for that you’ll need to code your own custom loader class.

Example with fetching data from Internet

This app will load a user data from jsonplaceholder.typicode.com and display it in a TextView.

Make sure you have enabled the INTERNET permission in the manifest xml file since we will be performing network task in the AsyncTaskLoader.

<uses-permission android:name="android.permission.INTERNET"/>

The layout xml file for the MainActivity, activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
        <TextView
            android:id="@+id/tvResult"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="Loading..."/>
</LinearLayout>

To do the actual task, create a class that extends the AsyncTaskLoader and implement the loadInBackground method. The return type will be whatever data you want to send back to the main app.

To trigger the work, the first thing to do is to implement the LoaderCallbacks interface in your Activity or Fragment. You can then call initLoader in the Activity's onCreate method.

The MainActivity.java and that’s it, the loader sample app is completed. This work sequence of this class a nutshell: initLoader -> onCreateLoader -> loadInBackground -> deliverResult -> onLoadFinished.

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<String> {
    private TextView tvResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvResult = (TextView) findViewById(R.id.tvResult);
        getSupportLoaderManager().initLoader(0, null, (LoaderManager.LoaderCallbacks<String>)this).forceLoad();
    }

    @Override
    public Loader<String> onCreateLoader(int id, Bundle args) {
        return new FetchData(this);
    }

    @Override
    public void onLoadFinished(Loader<String> loader, String data) {
        tvResult.setText(data);
    }

    @Override
    public void onLoaderReset(Loader<String> loader) {}

    private static class FetchData extends AsyncTaskLoader<String> {
        private String data;
        private String TAG = "TAG";

        @Override
        protected void onStartLoading() {
            if (data != null) {
                // use cached data
                deliverResult(data);
            } else {
                // we have no data, so kick off loading it
                forceLoad();
            }
        }

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

        @Override
        public String loadInBackground() {
            HttpURLConnection urlConnection = null;
            BufferedReader reader = null;
            String str = null;
            String line;
            try {
                URL url = new URL("http://jsonplaceholder.typicode.com/users/1");
                urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.connect();

                // Read the input stream into a String
                InputStream inputStream = urlConnection.getInputStream();
                StringBuffer buffer = new StringBuffer();
                if (inputStream == null) return null;

                reader = new BufferedReader(new InputStreamReader(inputStream));
                while ((line = reader.readLine()) != null) buffer.append(line);

                if (buffer.length() == 0) return null;
                str = buffer.toString();
                data = str;
            } catch (IOException e) {
                Log.e(TAG, "Error ", e);
                return null;
            } finally {
                if (urlConnection != null) urlConnection.disconnect();
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (final IOException e) {
                        Log.e(TAG, "Error closing stream", e);
                    }
                }
            }

            return str;
        }

        @Override
        public void deliverResult(String data) {
            super.deliverResult(data);
        }
    }
}

Looks pretty similar to an AsyncTask, but we can now hold onto results in a member variable and immediately return them back after configuration changes by immediately calling deliverResult() in our onStartLoading() method. Note how we don’t call forceLoad() if we have cached data - this is how we save ourselves from constantly reloading the data!

The main class implements LoaderManager.LoaderCallbacks<String>, where the String indicates the data returned by the loader will be a string, you are free to change it to other data types such as your own custom object, as long as you make sure your callback methods are also defined with the same data type for return type or method arguments.

The FetchData is an inner class of the MainActivity class. The method loadInBackground() method does the heavy lifing of getting the data from the network and the method deliverResult() as the name suggests, it delivers the data returned from loadInBackground().

Example with ListView and BaseAdapter

This example will walk through the Android AsyncTaskLoader example with ListView and BaseAdapter. AsyncTaskLoader uses AsyncTask to perform work. ListView is a view which shows items in vertically scrolling list. BaseAdapter is a common implementation for other adapters. While using AsyncTaskLoader we need to extend it and override at least a method i.e loadInBackground which perform the user task. After loading data we will set data to adapter and finally call notifyDataSetChanged() to reflect list view on UI.

File Movie.java

public class Movie {
    public int id;
    public String title;
    public Movie(int id, String title) {
        this.id = id;
        this.title = title;
    }
}

File MovieLoader.java

public class MovieLoader extends AsyncTaskLoader<List<Movie>> {
    public MovieLoader(Context context) {
        super(context);
    }
    @Override
    public List<Movie> loadInBackground() {
        List<Movie> list = new ArrayList<Movie>();
        list.add(new Movie(1, "The Shawshank Redemption"));
        list.add(new Movie(2, "The Godfather"));
        list.add(new Movie(3, "The Godfather: Part II"));
        return list;
    }
}

File MovieAdapter.java

public class MovieAdapter extends BaseAdapter {
    private LayoutInflater inflater;
    private List<Movie> items = new ArrayList<Movie>();
    public MovieAdapter(Context context, List<Movie> items) {
        this.items = items;
        inflater = LayoutInflater.from(context);
    }
    @Override
    public View getView(int position, View view, ViewGroup parent) {
        Movie m = (Movie) getItem(position);
        if (view == null) {
            view = inflater.inflate(R.layout.movie_item, null);
        }
        TextView tvTitle = (TextView) view.findViewById(R.id.tvTitle);
        tvTitle.setText(m.title);
        return view;
    }
    @Override
    public Object getItem(int position) {
        return items.get(position);
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public int getCount() {
        return items.size();
    }
    public void setItems(List<Movie> data) {
        items.addAll(data);
        notifyDataSetChanged();
    }
}

File MainActivity.java

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<Movie>> {
    MovieAdapter adapter;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        adapter = new MovieAdapter(this, new ArrayList<Movie>());
        ListView lvMovies = (ListView) findViewById(R.id.lvMovies);
        lvMovies.setAdapter(adapter);
        getSupportLoaderManager().initLoader(1, null, this).forceLoad();
    }
    @Override
    public Loader<List<Movie>> onCreateLoader(int id, Bundle args) {
        return new MovieLoader(MainActivity.this);
    }
    @Override
    public void onLoadFinished(Loader<List<Movie>> loader, List<Movie> data) {
        adapter.setItems(data);
    }
    @Override
    public void onLoaderReset(Loader<List<Movie>> loader) {
        adapter.setItems(new ArrayList<Movie>());
    }
}

File movie_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tvTitle"
        android:textSize="25sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

File activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/lvMovies"
        android:layout_height="match_parent"
        android:layout_width="match_parent"/>
</LinearLayout>

Loaders for Long Running Background Tasks

For super long running background tasks like a network operation, don’t use Loaders as they’re strictly attached to their Activity/Fragment context. If the Activity or Fragment is destroyed, they’ll also be destroyed. It’s better to make use of an IntentService in such a case which also executes on a background thread and is not tied to its client component. It can be also started with various flags to restart, if killed by the system.

Links