Lesson 135. Loader. LoaderManager. AsyncTaskLoader

Lesson 135. Loader. LoaderManager. AsyncTaskLoader


In this lesson:

– We study Loader and AsyncTaskLoader

Laaders appeared in the third version of Android. Designed for asynchronous operations and tied to some Activity or Fragment lifecycle methods.

I once tried to overcome this topic, but it did not work: I did not particularly understand the meaning and mechanisms. But Android creators don’t take a nap. They have declared some methods for working with the database obsolete and highly recommend using CursorLoader. Because of this, I now need to revise Lesson 52. And I decided that first it would make sense to sort out and highlight the topic of Loaders, and then I would already update Lesson 52.

So we have two classes.

LoaderManager – built in Activity and Fragment. As its name implies, it manages Loader objects. It creates, saves, destroys and starts / stops them. It uses the LoaderCallbacks interface to interact with it.

Loader – an object that must be able to perform asynchronously any task.

Let’s write a program in which we use a loader, and look at its behavior in the examples. The Laader will simply determine the current time, but it will be asynchronous and format-driven.

Let’s create a project:

Project name: P1351_Loader
Build Target: Android 4.0
Application name: Loader
Package name: ru.startandroid.develop.p1351loader
Create Activity: MainActivity

IN strings.xml add rows:

Short
Long
Get time
Observer

screen main.xml:



    
    
    
        
        
        
        
    
    
    

Time display text, format selection: short and long, time button and Observer button, which we will try to attach to the launcher.

Let’s create a class of a loader. And not in MainActivity, but separately, for clarity. In general, it is possible to create MainActivity as well, but there are limitations: it must be static. Otherwise LoaderManager swears: “Object returned from onCreateLoader must not be a non-static inner member class“.

TimeLoader.java:

package ru.startandroid.develop.p1351loader;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import android.content.Context;
import android.content.Loader;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

public class TimeLoader extends Loader {

  final String LOG_TAG = "myLogs";
  final int PAUSE = 10;

  public final static String ARGS_TIME_FORMAT = "time_format";
  public final static String TIME_FORMAT_SHORT = "h:mm:ss a";
  public final static String TIME_FORMAT_LONG = "yyyy.MM.dd G 'at' HH:mm:ss";

  GetTimeTask getTimeTask;
  String format;

  public TimeLoader(Context context, Bundle args) {
    super(context);
    Log.d(LOG_TAG, hashCode() + " create TimeLoader");
    if (args != null)
      format = args.getString(ARGS_TIME_FORMAT);
    if (TextUtils.isEmpty(format))
      format = TIME_FORMAT_SHORT;
  }

  @Override
  protected void onStartLoading() {
    super.onStartLoading();
    Log.d(LOG_TAG, hashCode() + " onStartLoading");
  }

  @Override
  protected void onStopLoading() {
    super.onStopLoading();
    Log.d(LOG_TAG, hashCode() + " onStopLoading");
  }

  @Override
  protected void onForceLoad() {
    super.onForceLoad();
    Log.d(LOG_TAG, hashCode() + " onForceLoad");
    if (getTimeTask != null)
      getTimeTask.cancel(true);
    getTimeTask = new GetTimeTask();
    getTimeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, format);
  }

  @Override
  protected void onAbandon() {
    super.onAbandon();
    Log.d(LOG_TAG, hashCode() + " onAbandon");
  }

  @Override
  protected void onReset() {
    super.onReset();
    Log.d(LOG_TAG, hashCode() + " onReset");
  }

  void getResultFromTask(String result) {
    deliverResult(result);
  }

  class GetTimeTask extends AsyncTask {
    @Override
    protected String doInBackground(String... params) {
      Log.d(LOG_TAG, TimeLoader.this.hashCode() + " doInBackground");
      try {
        TimeUnit.SECONDS.sleep(PAUSE);
      } catch (InterruptedException e) {
        return null;
      }

      SimpleDateFormat sdf = new SimpleDateFormat(params[0],
          Locale.getDefault());
      return sdf.format(new Date());
    }

    @Override
    protected void onPostExecute(String result) {
      super.onPostExecute(result);
      Log.d(LOG_TAG, TimeLoader.this.hashCode() + " onPostExecute "
          + result);
      getResultFromTask(result);
    }

  }
}

The Laader will receive time asynchronously. At the same time we will emulate a pause of a long execution, as if it goes to some thread and the server receives the data from there. I paused for 10 seconds, but you can put less so you don’t have to wait long for examples. It will be able to output time in two formats – short and long, respectively, the constants TIME_FORMAT_SHORT and TIME_FORMAT_LONG.

Our class extends the Loader class. Loader is a parameterized class, so we need to specify in the brackets <> a class-type that indicates that the returner will return after its operation. Our loader will return the line with time, so I specify String here.

IN designers we read time format data from the Bundle. If nothing came, we will use a short format.

The following are 5 standard methods of a loader.

onStartLoading – Invoked at start (onStart) Activity or a fragment to which the Loader will be attached.

onStopLoading – Invoked when stopping (onStop) Activity or the fragment to which the Loader will be attached.

Immediately to determine the formulation of states. We will assume that the loader entered the “started” state after the onStartLoading method and in the “stopped” state after the onStopLoading method. This is necessary because the behavior of the Loader depends on the condition and we will need to identify these states as verbal in the future.

It should be understood that two of these methods do not automatically mean that the launcher has started or ended. It is just a transition to a state started and stopped. And whether it will work or not at this time will determine you.

onForceLoad – In this method, I encode the work of the launcher. We launch GetTimeTask here, which will be our time to receive asynchronously. Below we will elaborate on what he is doing.

onAbandon – The method means that the loader becomes inactive. The example below will show what this means.

onReset – means the destruction of the loader, caused by closing (onDestroy) Activity or the fragment to which the Loader will be attached. Not called if onDestroy was called, such as when changing orientation.

Next we look at examples and see when and what methods are called.

method getResultFromTask is our method. GetTimeTask, after completing its work, will call this method and give us the results of its work. And we already call in it the standard method of the loader – deliverResult, which notifies the listener connected to the loader that the work is finished and transmits data to him.

GetTimeTask is an AsyncTask that takes the date format and returns (via getResultFromTask) to the launcher the current time in that format after a pause.

MainActivity.java:

package ru.startandroid.develop.p1351loader;

import android.app.Activity;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Loader;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.RadioGroup;
import android.widget.TextView;

public class MainActivity extends Activity implements LoaderCallbacks {

  final String LOG_TAG = "myLogs";
  static final int LOADER_TIME_ID = 1;

  TextView tvTime;
  RadioGroup rgTimeFormat;
  static int lastCheckedId = 0;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    tvTime = (TextView) findViewById(R.id.tvTime);
    rgTimeFormat = (RadioGroup) findViewById(R.id.rgTimeFormat);

    Bundle bndl = new Bundle();
    bndl.putString(TimeLoader.ARGS_TIME_FORMAT, getTimeFormat());
    getLoaderManager().initLoader(LOADER_TIME_ID, bndl, this);
    lastCheckedId = rgTimeFormat.getCheckedRadioButtonId();
  }

  @Override
  public Loader onCreateLoader(int id, Bundle args) {
    Loader loader = null;
    if (id == LOADER_TIME_ID) {
      loader = new TimeLoader(this, args);
      Log.d(LOG_TAG, "onCreateLoader: " + loader.hashCode());
    }
    return loader;
  }

  @Override
  public void onLoadFinished(Loader loader, String result) {
    Log.d(LOG_TAG, "onLoadFinished for loader " + loader.hashCode()
        + ", result = " + result);
    tvTime.setText(result);
  }

  @Override
  public void onLoaderReset(Loader loader) {
    Log.d(LOG_TAG, "onLoaderReset for loader " + loader.hashCode());
  }

  public void getTimeClick(View v) {
    Loader loader;

    int id = rgTimeFormat.getCheckedRadioButtonId();
    if (id == lastCheckedId) {
      loader = getLoaderManager().getLoader(LOADER_TIME_ID);
    } else {
      Bundle bndl = new Bundle();
      bndl.putString(TimeLoader.ARGS_TIME_FORMAT, getTimeFormat());
      loader = getLoaderManager().restartLoader(LOADER_TIME_ID, bndl,
          this);
      lastCheckedId = id;
    }
    loader.forceLoad();
  }

  String getTimeFormat() {
    String result = TimeLoader.TIME_FORMAT_SHORT;
    switch (rgTimeFormat.getCheckedRadioButtonId()) {
    case R.id.rdShort:
      result = TimeLoader.TIME_FORMAT_SHORT;
      break;
    case R.id.rdLong:
      result = TimeLoader.TIME_FORMAT_LONG;
      break;
    }
    return result;
  }

  public void observerClick(View v) {
  }

}

IN onCreate we get the LoaderManager object using the getLoaderManager method and call it the initLoader method, which will create and return the Loader to us. The parameters of the initLoader method are:
– ID of the loader, this is necessary because we can easily use several different launchers at once, both LoaderManager and ourselves have to differentiate them somehow.
– Bunlde object. In it you put the data you want to use when creating a loader
is an object that implements the LoaderCallbacks backbone interface. It will be used to interact with the loader.

So let’s see what we passed in initLoader. We use the LOADER_TIME_ID constant as the ID. In Bundle we put the time format we want to get. To determine the format we use our getTimeFormat () method, we will discuss it below. And the third parameter of the method is MainActivity, which implements the LoaderCallbacks interface. Just in case I will explain that it was possible to create a separate object for this kolbeck, but not to use Activity. To whom it is more convenient.

The initLoader method returns the created launcher, but I don’t store it anywhere because I don’t need it here.

The LoaderCallbacks interface requires three methods to be implemented:

onCreateLoader – Called when you need to create a new launcher, such as when we call the initLoader method above. The input receives the ID of the required Loader and Bundle with data. That is, the same objects we passed to initLoader.

onLoadFinished – Fires when the Loader finishes work and returns the result. At the entrance comes the loader and the result of his work.

onLoaderReset – Fires when LoaderManager is about to destroy the launcher. The entrance gets a loader.

The following examples illustrate the procedure for calling these three methods.

method getTimeClick – Get time button. In it, we determine in what format it takes time. Next we check if the last created loader was created using the same format, then just get the loader by the getLoader method by ID. If the format is different, then we need a new launcher. The restartLoader method is used for this purpose. It accepts the same parameters as initLoader and creates a new launcher. Next, we call the forceLoad method, thus starting the job.

As you can see, LoaderManager has three other methods for getting a loader: getLoader, initLoader, and restartLoader. Let’s talk about their differences right away.

getLoader – simply get a loader with the specified ID. If a loader with this ID has not yet been created, then the method returns null.

initLoader – create a loader if it did not exist. If the launcher existed, then the method will return it, only replacing the callback object that you pass to the method. And if the launcher not only existed, but already had time to work out, then the last result will be sent to the onLoadFinished method.

restartLoader – create a new loader anyway. A little later we will look at examples of what happens if you create a new one when running a loader.

I hope that now the logic of the getTimeClick method has become clearer.

method getTimeFormat simply returns the time format depending on the format selected on the screen.

method observerClick until left blank. Let’s fill in later.

I have added logs to almost all methods to see how the methods are executed. And the hash codes of the loaders will allow us to see for which loaders these methods are performed.

We save everything, launch the application.

The screen does not display the time, because the launcher has just moved to the “started” state, but did not start working.

In the logs:

1091125312 create TimeLoader
onCreateLoader: 1091125312
1091125312 onStartLoading

We see that the initLoader method in onCreate called the onCreateLoader method, in which the TimeLoader constructor was called. And the onStartLoading method worked when starting Activity.

We call the application home by clicking Home.

1091125312 onStopLoading

Again we will open from the list of the last

1091125312 onStartLoading

Close the application with the Back button

1091125312 onStopLoading
1091125312 onReset

It is seen that Activity in its lifecycle methods calls the appropriate methods of the Laader: at start – onStartLoading, at stop – onStopLoading, and at closing – onReset.

Note that it was not typed onLoaderReset. It is only called when data has been received at least once. We will see in the examples below.

Let’s see a loader at work. Run the application again, leave the Short time format and click Get time. We wait 10 seconds and see the result on the screen.

We look at the logs of work:

1091254864 onForceLoad
1091254864 doInBackground
1091254864 onPostExecute 10:57:15 PM
onLoadFinished for loader 1091254864, result = 10:57:15 PM

OnForceLoad was started and tested by AsyncTask, and the backbone of the loader got the result in onLoadFinished.

Let’s try again. But now let’s see what happens if you minimize the program when running the loader.

Press Get time and immediately turn the application home.

1091254864 onForceLoad
1091254864 doInBackground
1091254864 onStopLoading
1091254864 onPostExecute 11:00:26 pm

We see that after onStopLoading AsyncTask returned the result, but onLoadFinished did not work anymore because the loader stopped.

Now let’s check what happens if we run the application we close the application.

Open the application, click Get time and close the application with the Back button.

1091254864 onForceLoad
1091254864 doInBackground
1091254864 onStopLoading
onLoaderReset for loader 1091254864
1091254864 onReset
1091254864 onPostExecute 11:03:00 PM

We see that after onStopLoading, the launcher was destroyed. This time, the onLoaderReset method worked, because this launcher was already receiving data. AsyncTask honestly worked and returned the result, but it is no longer interesting to anyone, because the loader is destroyed.

Now let’s check the restartLoader method.

Open the application, click Get time. Wait for the loader to run and show time, then switch the format to Long and press Get time again and wait for the new loader to work out.

We look at the logs:

1091662504 create TimeLoader
onCreateLoader: 1091662504
1091662504 onStartLoading
1091662504 onForceLoad
1091662504 doInBackground
1091662504 onPostExecute 11:08:42 pm
onLoadFinished for loader1091662504, result = 11:08:42 PM
1091662504 onAbandon
1091700592 create TimeLoader
onCreateLoader: 1091700592
1091700592 onStartLoading
1091700592 onForceLoad
1091700592 doInBackground
1091700592 onPostExecute 2013/11/04 AD at 23:08:56
onLoadFinished for loader1091700592, result = 2013.11.04 AD at 23:08:56
1091662504 onReset

The hash codes show that we had two loaders in our work: the first – 1091662504, the second – 1091700592 (you may have other hash codes).

When creating the second loader, the first was called the onAbandon method, which means that the first loader becomes obsolete and is no longer current. That is, the getLoader method will not return it. Now the current loader is the second. The following is a standard set of methods for the operation of the second loader, and when it successfully fulfills and returns the result, the onReset method of the first loader is called. That is, after the successful operation of the second loader, the first loider is destroyed.

Let’s see what happens if you create a second launcher until you finish the first one.

We leave the Long format, press Get time, then immediately switch to Short and press Get time again and wait for the result. We look at the logs.

1091700592 onForceLoad
1091700592 doInBackground
1091700592 onAbandon
1091713440 create TimeLoader
onCreateLoader: 1091713440
1091713440 onStartLoading
1091713440 onForceLoad
1091713440 doInBackground
1091700592 onPostExecute 2013/11/04 AD at 23:16:39
1091713440 onPostExecute 11:16:41 pm
onLoadFinished for loader 1091713440, result = 11:16:41 PM
1091700592 onReset

Now the first loader is 1091700592, the second is 1091713440. The scheme is generally the same: the first loader is translated into the category of old (onAbandon) when creating the second. But the result in onLoadFinished is counted only in the second because it is current and the result of the old loader will be ignored.

Now try rotating the device to change the screen orientation. We see the last result on the screen again. We look at the log:

onLoadFinished for loader 1091713440, result = 11:16:41 PM

When turning the screen, we had an initLoader in onCreate and called onLoadFinished with the latest result of the current launcher. Accordingly, if you launch a new launcher and while it is running, you will return the screen, then you will not return, because this launcher has no result yet.

ContentObserver

Loaders can handle ContentObserver objects. This is an object that will tell you that the data you are interested in has changed and it makes sense to read it again.

The Loader has its own implementation of this class: ForceLoadContentObserver. When it receives a notification that the data has been modified, it will depend on the status of the loader:
– if the launcher is in the onStartLoading state, then the forceLoad method is called, which should consider this new data
– if the Loader is stopped (onStopLoading), then the label indicates that the data has been changed and the Laader at startup can read this label and start all the same forceLoad to read the data

We will add contentObserver to our application. For this purpose in MainActivity we realize the Observer button handler:

  public void observerClick(View v) {
    Log.d(LOG_TAG, "observerClick");
    Loader loader = getLoaderManager().getLoader(LOADER_TIME_ID);
    final ContentObserver observer = loader.new ForceLoadContentObserver();
    v.postDelayed(new Runnable() {
      @Override
      public void run() {
        observer.dispatchChange(false);
      }
    }, 5000);
  }

Create an instance of ForceLoadContentObserver emulation situation: after 5 seconds it will inform us that the data has changed.

And in the loader we will rewrite onStartLoading:

  @Override
  protected void onStartLoading() {
    super.onStartLoading();
    Log.d(LOG_TAG, hashCode() + " onStartLoading");
    if (takeContentChanged())
      forceLoad();
  }

Here we read (and at the same time reset) the label using the takeContentChanged method. If the label says that the data has been changed (true), then we run.

We save everything, launch the application. Click Observer, wait for the result and see the logs:

1091644064 create TimeLoader
onCreateLoader: 1091644064
1091644064 onStartLoading
observerClick
1091644064 onForceLoad
1091644064 doInBackground
1091644064 onPostExecute 10:31:26 pm
onLoadFinished for loader 1091644064, result = 10:31:26 PM

We see that after 5 seconds the Observer has launched the forceLoad method. Then everything is as usual.

Now click Observer again and immediately turn the application home.

observerClick
1091644064 onStopLoading

The Observer also worked here, but the forceLoad method did not start because the loader was stopped. In this case, check that the data has changed. And now we consider this label.

Open the program from the list of the latter

1091644064 onStartLoading
1091644064 onForceLoad
1091644064 doInBackground
1091644064 onPostExecute 10:32:42 pm
onLoadFinished for loader 1091644064, result = 10:32:42 PM

The application was restarted, with onStartLoading working, in which we read the label, realized that the data had changed, and started onForceLoad.

Finally a couple more words.

Important note! All the examples above work if the initLoader is called in onCreate. If you are interested, try reworking the application logic by removing initLoader from onCreate and the launcher will behave differently, its relationship with lifecycle methods will change.

You can kill the launcher manually using the destroyLoader method.

All of the above should work with snippets (they have their own getLoaderManager method) and with android.support.v4.app.FragmentActivity (getSupportLoaderManager method).

AsyncTaskLoader

This is a loader that will do its job asynchronously and return you the result. The TimeLoader class we made is basically a simplified version of AsyncTaskLoader because it does its job in AsyncTask as well. But to avoid messing with AsyncTask every time, there is an AsyncTaskLoader.

let’s create a class TimeAsyncLoader.java:

package ru.startandroid.develop.p1351loader;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import android.content.AsyncTaskLoader;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

public class TimeAsyncLoader extends AsyncTaskLoader {

  final String LOG_TAG = "myLogs";
  final int PAUSE = 10;

  public final static String ARGS_TIME_FORMAT = "time_format";
  public final static String TIME_FORMAT_SHORT = "h:mm:ss a";
  public final static String TIME_FORMAT_LONG = "yyyy.MM.dd G 'at' HH:mm:ss";

  String format;

  public TimeAsyncLoader(Context context, Bundle args) {
    super(context);
    Log.d(LOG_TAG, hashCode() + " create TimeAsyncLoader");
    if (args != null)
      format = args.getString(ARGS_TIME_FORMAT);
    if (TextUtils.isEmpty(format))
      format = TIME_FORMAT_SHORT;
  }

  @Override
  public String loadInBackground() {
    Log.d(LOG_TAG, hashCode() + " loadInBackground start");
    try {
      TimeUnit.SECONDS.sleep(PAUSE);
    } catch (InterruptedException e) {
      return null;
    }
    SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
    return sdf.format(new Date());
  }

}

Exactly the same functionality as in TimeLoader, only now we just put the working code into the loadInBackground method. I will no longer redefine in this class and pledge all its basic methods.

Minimal changes will be required to use this launcher in MainActivity. It is necessary to specify simply in onCreateLoader that by LOADER_TIME_ID it is necessary to create not TimeAoyncLoader, not TimeLoader:

  @Override
  public Loader onCreateLoader(int id, Bundle args) {
    Loader loader = null;
    if (id == LOADER_TIME_ID) {
      loader = new TimeAsyncLoader(this, args);
      Log.d(LOG_TAG, "onCreateLoader: " + loader.hashCode());
    }
    return loader;
  }

That’s all.

Run the example, it will work the same way, but now it will be used not TimeLoader but TimeAsyncLoader. Well, there will be less logs.

The AsyncTaskLoader class has a cancellation method: cancelLoad. A canceled launcher will no longer be onLoadFinished, but onCanceled in AsyncTaskLoader after work has finished.

There is also a setUpdateThrottle method that will delay between two consecutive calls of the same launcher. That is, for example, you set this delay at 10,000 ms. Then you start the launcher, it works. And you immediately try to run it again. But it won’t start. It will count down 10 seconds after the last run is completed, and then it will start working again.

In the next lesson:

– we use CursorLoader




Discuss in the forum [14 replies]

Leave a Comment