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:
Application
context, hence the Activity Context
object undergoing a configuration change is not preserved causing memory leak.Loader API involves following classes
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
Loader
Loader
finishes loading the dataThe 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 instanceonLoadFinished()
- this is where the results you deliver appearonLoaderReset()
- your chance to clean up any references to the now reset Loader dataLoader. 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
.
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.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