Open menu with table of contents Asynchronous Tasks
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Asynchronous Tasks

Stuttgart Media University

1 Agenda

  • General Rules for Asynchronous Tasks
  • Deprecated AsyncTask
  • The Main- / UI-Thread
  • AsyncTaskLoader
  • Jetpack's WorkManager

2 General Rules for Asynchronous Tasks

  • Never block the main / UI thread
  • Do asynchronous / long lasting tasks in the background
  • Do UI-changes only in the main / UI thread
  • Take into account app restarts and termination
  • Expect errors and long-lasting processes

3 UI-Thread

  • Android modifies the user interface and handles input events from one single user interface thread. This thread is also called the main thread.
  • Android collects all events in a queue and dispatches them to be executed in the main thread

    center

4 UI-Thread

  • Long running tasks can block the UI and events (e.g. touches, keystrokes) can not be processed
  • If the UI is blocked the "Application is Not Responding" (ANR) dialog will be shown

center

5 UI-Thread

  • A better way is to execute long running tasks in a separate thread
  • But beware: The UI must not be manipulated outside the Main-Thread!

    center

5.1 Deprecated AsyncTask

Before WorkManager was introduced many applications used implementations of AsyncTask and AsyncTaskLoader. The implementation of AsyncTask is now deprecated.

AsyncTaskLoader on the other hand is still in use in some cases. Fore more information see AsyncTaskLoader documentation.

It is recommended to use the WorkManager and / or Kotlin Coroutines (if Kotlin used) in most cases.

5.2 Options for Updating the UI in Background

  • To update the UI from a background thread use design patterns or one of the following alternatives that post a request for updating the UI to the UI queue:
    • Activity.runOnUIThread(Runnable)
    • View.post(Runnable)
    • View.postDelayed(Runnable, long)

      center

Note: Using the above alternatives can become hard to maintain in a more complex application.

5.3 Thread Example

Below is an example for loading an image in background using a Thread (not recommended in Android, see next slides):

fun onClick(v: View?) {
  Thread {
    val bitmap: Bitmap = loadImageFromNetwork("http://example.com/image.png")
    mImageView.post(Runnable { mImageView.setImageBitmap(bitmap) })
  }.start()
}

5.4 AsyncTaskLoader

The Gradle import used for AsyncTaskLoader:

implementation("androidx.loader:loader:1.1.0")

Example implementation for AsyncTaskLoader:

import android.os.Bundle;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;

class MainActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks<String?> {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val loader: Loader<String?> = supportLoaderManager.initLoader(LOADER_ID, null, this)
    if (loader != null) {
      loader.forceLoad()
    }
  }

  override fun onCreateLoader(id: Int, @Nullable args: Bundle?): Loader<String> {
    return MyDataLoader(this@MainActivity)
  }

  override fun onLoadFinished(loader: Loader<String?>, data: String?) {
    val textView = findViewById<TextView>(R.id.myTextView)
    textView.text = data
  }

  override fun onLoaderReset(loader: Loader<String?>) {
    val textView = findViewById<TextView>(R.id.myTextView)
    textView.text = ""
  }

  companion object {
    private const val LOADER_ID = 42
  }
}

5.5 AsyncTaskLoader

import android.content.Context;
import android.os.SystemClock;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;

class MyDataLoader(context: Context) :
  AsyncTaskLoader<String?>(context) {
  override fun loadInBackground(): String? {
    //do your long running task here
    SystemClock.sleep(2000)
    return "My loaded data"
  }
}

6 Jetpack's WorkManager

With Android Jetpack Google introduced the WorkManager for asynchronous task processing (aka. persistent work).

A detailed guide for WorkManager can be found here.

Dependencies necessary for using the WorkManager:


dependencies {
    def work_version = "2.7.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

6.1 Benefits of WorkManager

The WorkManager brings a few benefits over the other options:

6.2 Types of Persistent Work

The WorkManager differentiates between 3 types of persistent work:

  • Immediate: Tasks that must begin immediately and complete soon. May be expedited.
  • Long Running: Tasks which might run for longer, potentially longer than 10 minutes.
  • Deferrable: Scheduled tasks that start at a later time and can run periodically.

6.3 Types of Persistent Work

Android WorkManager 60%

Source: Android Developers Documentation

6.4 Work Constraints

Work constraints allow the execution of work under optimal conditions. There are 5 work constraints provided:

  • NetworkType
  • BatteryNotLow
  • RequiresCharging
  • DeviceIdle
  • StorageNotLow

For more information see Work Constraints.

6.5 Work Constraints

To create a set of constraints you can use the Constraints.Builder():

Constraints constraints = new Constraints.Builder()

    // require to be connected to a WiFi
    .setRequiredNetworkType(NetworkType.UNMETERED)

    // require to be in charging mode
    .setRequiresCharging(true)

    .build();

The work will be executed only when all defined constraints are met.

6.6 Work Scheduling

When scheduling work for execution WorkRequests are created. There are two derived classes of WorkRequest:

  • OneTimeRequest for scheduling non-repeating work
  • PeriodicWorkRequest for scheduling work that repeats on some interval
WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);

// or for complex work
WorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(MyWork.class)
    // Additional configuration like constraints and retry policies
    .build();

6.7 Retry and Backoff Policies

Retry policies allow work to be re-executed on failure. For that, a backoff delay and backoff policy can be defined in setBackoffCriteria():

  • backoffPolicy can be either LINEAR or EXPONENTIAL
  • backoffDelay is a number (cannot be less than 10 seconds)
  • timeUnit is the TimeUnit of backoffDelay

Once a worker returns Result.retry() the defined policy applies.

6.8 Retry and Backoff Policies

Example for adding a retry and backoff policy:

WorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWork.class)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    ).build();

6.9 Worker Implementation Example

public class WeatherDataWorker extends Worker {
    
  public static final String KEY_LATITUDE = "lat";
  public static final String KEY_LONGITUDE = "long";

  private final Double latitude;
  private final Double longitude;

  public WeatherDataWorker(
          @NonNull Context context,
          @NonNull WorkerParameters params
  ) {
    super(context, params);

    // Get data from params
    latitude = params.getInputData().getDouble(KEY_LATITUDE, 0.0);
    longitude = params.getInputData().getDouble(KEY_LONGITUDE, 0.0);
    
    // Optionally check if data is valid
    // ...
  }

  @Override
  public Result doWork() {

    // Do the work here, e.g. fetch weather data
    try {
      Map<String, Object> weatherData = getWeatherData(latitude, longitude);

      if (weatherData == null) {
        // Weather data could not be fetched, retry
        return Result.retry();
      }

      // Prepare data result (limited to 10KB)
      Data data = new Data.Builder()
              .putAll(weatherData)
              .build();

      // Weather data fetched successfully.
      return Result.success(data);
    } catch (Exception exception) {
      // Some error occurred, abort
      return Result.failure();
    }
  }
  
  // ...
}

6.10 Worker Implementation Example

Call the worker then from your activity as follow:

// method in activity class
private void loadWeatherData(Double latitude, Double longitude) {

    // Prepare input data
    Data inputData = new Data.Builder()
            .putDouble(KEY_LATITUDE, latitude)
            .putDouble(KEY_LONGITUDE, longitude)
            .build();

    // Define Constraints (require network connectivity)
    Constraints constraints = new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build();

    // Create work request
    WorkRequest weatherWorkRequest = new OneTimeWorkRequest.Builder(WeatherDataWorker.class)
            .setInputData(inputData)
            .setConstraints(constraints)
            .setBackoffCriteria(
                    BackoffPolicy.LINEAR,
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                    TimeUnit.MILLISECONDS)
            .build();

    // Get WorkManager
    WorkManager workManager = WorkManager.getInstance(this);
            
    // Enqueue request and get result
    workManager.enqueue(weatherWorkRequest);

    // Listen with live data to result (listening should happen in ViewModel)
    workManager.getWorkInfoByIdLiveData(weatherWorkRequest.getId())
            .observe(this, info -> {
                if (info != null && info.getState().isFinished()) {
                    // Result data is stored in info.getOutputData()
                }
            });
}

7 Kotlin Coroutines and Lifecycle-Aware Components

Other options that are easily maintained are Kotlin Coroutines and lifecylce-aware components. These options are not handled in this lecture but very important to know when developing an Android application.

You can find more about Coroutines in the official Kotlin documentation as well in the Android documentation.

For lifecyle-aware components you can read Handling Lifecycles with Lifecycle-Aware Components.

8 Summary

  • Working with multithreading is very important to achieve responsive applications
    • Unfortunately it makes programming more complex and error prone
  • To simplify and improve asynchronous programming Android provides the WorkManager
  • The WorkManager differentiates between 3 persistent work types, depending of when and how long a task should be executed
  • Work constraints can be used to run specific work only under optimal conditions

9 Recap Questions

  • When would you use a Service and when an AsyncTaskLoader?
  • Why is it not allowed to manipulate the UI outside of the UI-Thread?
  • What happens when you do long running tasks on the UI-Thread?
  • What configuration options does the WorkManager provide? What do these options configure?