As device performance improves and the software ecosystem evolves, more and more Android applications need to perform relatively complex network, asynchronous, and offline tasks. For example, if a user wants to watch a video offline, but does not want to stay in the app interface waiting for the download to complete, there needs to be a way for these offline processes to run in the background. For example, if you want to share a great Vlog on social media, you will want to upload the video without affecting your device. This brings us to today’s topic: managing back office and front office work with WorkManager.

If you prefer to see this in video, check it out here.

This article focuses on the WorkManager API and its usage to give you insight into how it works and how it can be used in real life development. Stay tuned for another post on how to make better use of WorkManager in Android Studio.

The WorkManager based API

Since the first stable release, WorkManager has provided basic apis to help you define work, queue it, execute it sequently, and notify your application when it’s done. Classified by function, these basic apis include:

Delay the

In the original release, these tasks could only be defined as deferred execution, meaning that they would be deferred to start executing after definition. With this deferred execution strategy, non-urgent or low-priority tasks will be deferred.

Deferred execution of WorkManager takes into account the low power consumption of the device and the standby storage partition of the application, so you don’t have to worry about when the work needs to be executed, just leave it to WorkManager.

Work constraints

WorkManager supports constraints on a given job run that ensure that work is deferred until the best conditions are met. For example, run only when the device has a non-pay-per-traffic network connection, when the device is idle, or when it has sufficient power. You can focus on developing other features of your application, leaving the checking of working conditions to the WorkManager.

Dependencies between the workplace

We know that there can be dependencies between jobs. For example, if you are developing a video editing application, users may need to share the clips to social media when they are finished, so your application needs to render several video clips in sequence and then upload them together to the video service. This process is sequential in that the upload work depends on the rendering work being done.

As another example, when your application has finished synchronizing data with the backend, you might want the local log files generated during the synchronization to be cleaned up as soon as possible, or to populate the local database with new data from the backend. You can then request the WorkManager to perform the work sequentially or in parallel, making the work seamless. The WorkManager will run subsequent workers after ensuring that all given conditions are met.

Work performed multiple times

Many applications with server synchronization capabilities are characterized by the fact that the synchronization between the application and the back-end server is not a one-time process, but may require multiple implementations. For example, when your application provides an online editing service, you must frequently synchronize local editing data to the cloud, which creates work that is performed regularly.

Working state

Because you can check the status of a job at any time, the entire life cycle is transparent for work that is performed regularly. You can tell if a job is in a queued, running, blocked, or completed state.

The WorkManager modern API

The basic API described above was already available when we released the first stable release of WorkManager. When we first talked about WorkManager at the Android Developer Summit, we thought of it as a library for managing extensible back-end work. That’s still true today from a bottom-up perspective. But then we added more features and made the API more in line with modern norms.

Executed immediately

Now, when your application is in the foreground, you can request that something be done immediately. Then even when the application is put in the background, the work is not interrupted but continues. So even if the user switches to another application, your application can still do things like add filters, save photos locally, upload photos, and so on.

For developers of large applications, more resources and efforts need to be put into optimizing resource usage. But WorkManager can greatly reduce their burden with good resource allocation strategies.

Multi-process API

With a new multi-process library to handle work, WorkManager introduced new apis and low-level optimizations to help large applications schedule and perform work more efficiently. This benefits from the new WorkManager’s ability to schedule and process more efficiently in a separate process.

Enhanced job test API

Testing is important before an app is released to the store or distributed to users. So we added an API to help you test individual workers or groups of workers with dependencies.

Tools to improve

Along with the library release, we also improved a number of developer tools. As a developer, you can use Android Studio directly to access detailed debug logs and check information.

Start using WorkManager

These new apis and improved tools, while providing greater convenience to developers, are also forcing us to rethink the best time to use WorkManager. While our core ideas for designing WorkManager are still correct from a technical point of view, the capabilities of WorkManager have far exceeded our design expectations for an increasingly complex development ecosystem.

The “persistent” nature of work

WorkManager can handle any type of work you assign to it, so it has evolved into a task-specific and reliable tool. The WorkManager executes the Worker you define in the global scope, which means that your work is retained as long as your application is running, whether it’s device orientation changes, Activity is reclaimed, etc. However, this alone does not qualify as “persistent”, so WorkManager also uses the Room database underneath to ensure that when the process is terminated or the device is restarted, your work can still be performed, and possibly continue from where it was interrupted.

Perform work that requires a long run

WorkManager version 2.3 introduced support for long-running jobs. When we talk about long running jobs, we mean jobs that run longer than the 10-minute execution window. Normally, the execution window of a Worker is limited to 10 minutes. In order to achieve long running work, the WorkManager bundles the life cycle of the Worker with the life cycle of the front desk service. The JobScheduler and the in-process Scheduler are still aware of this work.

Because the front desk service controls the lifecycle of work execution, and the front desk service needs to show notifications to users, we added apis to WorkManager. Users’ attention spans are limited, so WorkManager provides apis that make it easy for users to stop long running work with notifications. Let’s examine a long-running working example with the following code:

class DownloadWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
    fun notification(progress: String): Notification = TODO()
    // The notification method generates an Android notification message based on progress information.
    suspend fun download(inputUrl: String,
      outputFile: String,
      callback: suspend (progress: String) - >Unit) = TODO()
    // Define a method for chunking downloads
    fun createForegroundInfo(progress: String): ForegroundInfo {
      return ForegroundInfo(id, notification(progress))
    }
 
    override suspend fun doWork(a): Result {
      download(inputUrl, outputFile) { progress -> 
        val progress = "Progress $progress %"
        setForeground(createForegroundInfo(progress))
      } // Provides a doWork method of the suspend tag, which calls the download method and displays the latest progress information.
      return Result.success() 
    } // After downloading, the Worker just needs to return success
}
Copy the code

Delta DownloadWorker class

There is a DownloadWorker class that extends from the CoroutineWorker class. We will define some helper methods in this class to simplify our work. The first is a Notification method that generates an Android notification message based on the given progress information. Next we define a method for chunking downloads that takes three parameters: the URL to download the file, the local location where the file is saved, and the suspend callback function. This callback is executed every time a block download status changes. The information carried in the callback can then be used to generate a notification.

With these helper methods, we can save the ForegroundInfo instance that the WorkManager needs to run its work for a long time. ForegroundInfo is constructed from a combination of notification IDS and notification instances. Continue with the code example above for the CoroutineWorker class.

In this code, we provide a doWork method of the suspend tag that calls the chunking download helper method just mentioned. Because we provide some up-to-date progress information each time the callback occurs, we can use this information to build the notifications and call the setForeground method to display them to the user. The operation calling setForeground here is the reason why the Worker runs for a long time. After downloading, the Worker only needs to return success, and then the WorkManager will decouple the execution of the Worker from the front-end service, clean up the notification message, and terminate the relevant service if necessary. Therefore, our Worker itself does not need to perform service management.

Terminate work that has been committed for execution

Users may suddenly change their minds, such as wanting to cancel a job. A notification of a foreground running service cannot be simply cancelled with a swipe. The previous approach was to add an action to the notification message that sends a signal to The WorkManager when the user clicks, terminating a job as intended by the user. You can also terminate by performing expedited work, as described below.

fun notification(progress: String): Notification {
  val intent = WorkManager.getInstance(context)
      .createCancelPendingIntent(getId())
  return NotificationCompat.Builder(applicationContext, id)
      .setContentTitle(title)
      .setContentText(progress)
      // Some other operations
      .addAction(android.R.drawable.ic_delete, cancel, intent)
      .build()
}
Copy the code

A DownloadWorker class derived from the CoroutineWorker class

The first step is to create an Intent to be handled, which can easily cancel a task. We need to call the getId method to get the job creation work request ID, and then call createCancelPendingIntent API to create this Intent. When the Intent is triggered, it sends a cancellation signal to the WorkManager to cancel the work.

The next step is to generate a notification message with a custom action. We use NotificationCompat. The title of the Builder set up notifications, and then add some text. The addAction method is then called to associate the “cancel” button in the notification with the Intent created in the previous step. Thus, when the user clicks the “Cancel” button, the Intent is sent to the foreground service currently executing the Worker, thus terminating it.

Perform urgent work

Android 12 introduced a new foreground service limitation that prevents foreground services from being started while the application is in the background. Therefore, starting from Android 12, calling the setForegroundAsync method raises a Foreground Service Start Not Allowed Exception. In this case, WorkManager comes in handy. Support for expedited work has been added to WorkManager version 2.7, so I’ll show you how to terminate committed work using WorkManager.

From the user’s perspective, expedited work is user-initiated and therefore very important to the user. These tasks need to be started even when the application is not in the foreground. For example, a chat application needs to download an attachment from a message, or the application needs to process a payment subscription. In versions of the API prior to Android 12, expedited jobs were performed by foreground services. Starting with Android 12, they will be performed by Expedited jobs.

The system limits the amount of rush work in the form of quotas. There are no quota limits for rush work when the application is in the foreground, but these limits must be followed when the application moves to the background. The size of the quota depends on the standby storage partition of the application and the importance (such as priority) of the process. Rush work is literally work that needs to start execution as soon as possible, which means that this type of work is latency sensitive and therefore does not support setting initial delays or scheduled execution. Because of quota restrictions, expedited work could not replace long-running work. When your users want to send an important message, WorkManager makes sure that message is sent as soon as possible.

class SendMessageWorker(context: Context, parameters: WorkerParameters): 
  CoroutineWorker(context, parameters) {
  override suspend fun getForegroundInfo(a): ForegroundInfo {
    TODO()
  }
    
  override suspend fun doWork(a): Result {
    TODO()
  }
}
Copy the code

△ Expedited work sample code

For example, an example of synchronizing chat application messages uses the expedited work API. The SendMessageWorker class extends from CoroutineWorker and is responsible for synchronizing messages from the background to the chat application. Rush work needs to be run in the context of a foreground service, much like long-running work in pre-12 versions of Android. Therefore, our Worker class also needs to implement the getForegroundInfo interface to facilitate the generation and display of notification messages. On Android 12, however, WorkManager does not display other notifications, because behind the Worker we define is a rush job implementation. You need to implement a doWork method of the suspend tag as usual. It is important to note that rush jobs may be interrupted after your application has occupied the full quota. Therefore, it is better for our Worker to keep track of certain states so that it can resume running after rescheduling.

val request = OneTimeWorkRequestBuilder<ForegroundWorker>()
    .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
    .build()
 
WorkManager.getInstance(context)
    .enqueue(request)
Copy the code

△ Sample code for setExpedited API

You can schedule expedited work using the setExpedited API, which tells The WorkManager that the user thinks a given work request is important. Because there is a quota limit on the amount of work that can be arranged, you need to indicate what to do when the applied quota is used up. There are two alternatives: turn the rush request into a regular work request, or abandon new work requests when the quota is used up.

WorkManager Multi-process API

Starting with version 2.5, WorkManager has made several improvements to applications that support multiple processes. You need to define dependencies for work-Multiprocess artifacts if you need to use the multi-process API, which aims to perform extensive initialization operations on redundant or expensive parts of the WorkManager in a worker process. SQLite contention can occur when multiple processes simultaneously acquire transaction locks for a unified underlying SQLite database. This contention is exactly what we want to reduce with the multi-process API. On the other hand, we also want to ensure that the in-process scheduler is running in the correct process.

To understand which parts of the WorkManager are redundant when it initializes, we need to know what it does in the background.

Initialization of a single process

△ Initialization of a single process

First observe a single process initialization process. The first thing that happens when an Application is started is that a platform calls the application.oncreate method. Then, at some point in the process life cycle, workManager.getInstance is called to initiate the initialization of WorkManager. When the WorkManager is initialized, we run ForceStopRunnable. This process is important because the WorkManager checks to see if the application has been forced to stop before and compares the information stored in the WorkManager with information in JobScheduler or AlarmManager to ensure that the jobs are properly scheduled for execution. At the same time, we can reschedule some work that was previously interrupted, such as some recovery work after a process crash. As you all know, this is very expensive and requires comparing and coordinating states across multiple subsystems, but ideally this operation should only be performed once. Also note that the in-process scheduler runs only in the default process.

Initialization of multiple processes

△ Multi-process initialization process

Then let’s see what happens if the application has a second process. If the application has a second process, it basically repeats the operations that were done in the first process. First, the first process is initialized as above, and since this is the primary process, the in-process Scheduler also runs In it. For the second process, we repeat the process we just did, calling Application.onCreate again, and reinitializing the WorkManager. This means that we will repeat all the work we did in the first process.

Based on the previous analysis, you might be wondering, why do YOU need to implement Force Table Runable again? This is because the WorkManager does not know which of these processes has a higher priority. If the application is an on-screen keyboard or Widget, the main process may not be the same as the default process. Also, no in-process scheduler is running in the secondary processes (because it is not the default process). The selection of the process in which the in-process scheduler resides is important, and since it is not restricted by other persistence schedulers, adjusting the process in which it resides can significantly improve data throughput. For example, JobScheduler has an upper limit of 100 jobs, whereas an in-process scheduler has no such limit.

val config = Configuration.Builder()
    .setDefaultProcessName("com.example.app")
    .build()
Copy the code

Example code for specifying the default process for the application

Define the main process through WorkManager

Let’s look at how to define the specified default process. This is usually the name of the application’s software package. Once the application’s default process is defined, the in-process scheduler will run in it. But what about helper processes? Is there any way to prevent the WorkManager from being reinitialized in it? It turns out it can be done. What we really need is not to initialize the WorkManager at all.

To achieve this, we introduced RemoteWorkManager. This class needs to bind to the specified process (the main process) and use the binding service to forward all work requests from the secondary process to the specified main process. This way, you can completely avoid all of the cross-process SQLite contention just mentioned, because there is only one process writing to the underlying SQLite database from start to finish. You can create the work request in the worker process as usual, but RemoteWorkManager should be used instead of WorkManager. When RemoteWorkManager is used, it is bound to the main process via a binding service and all work requests are forwarded and stored in a specific queue for execution. You can implement this binding by incorporating the RemoteWorkManager service into your application’s Android Manifest RXML.

val request = OneTimeWorkRequestBuilder<DownloadWorker>()
    .build()
 
RemoteWorkManager.getInstance(context)
    .enqueue(request)
Copy the code

△ Use RemoteWorkManager sample code

<! -- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkManagerService"
    android:exported="false" />
Copy the code

△ Manifest registration service sample code

The Worker runs in different processes

We’ve seen how to avoid contention by defining the main process with WorkManager, but sometimes you also want to be able to run workers in different processes. For example, if you are running ML pipelines in an application’s helper process and the application has dedicated interface processes, you may need to run different workers in different processes. For example, isolating a task in a helper process so that if something goes wrong in the process and crashes, the rest of the application will not crash and exit altogether, especially if the interface process works properly. To implement Worker execution in different processes, you need to extend the RemoteCoroutineWorker class. This class is similar to CoroutineWorker, and you’ll need to implement the doRemoteWork interface yourself.

public class IndexingWorker(
  context: Context,
  parameters: WorkerParameters
): RemoteCoroutineWorker(context, parameters) {
  override suspend fun doRemoteWork(a): Result {
    doSomething()
    return Result.success()
  }
}
Copy the code

△ IndexingWorker class sample code

Since this method is executed in a Worker process, we still need to define which process the Worker needs to bind to. To do this, we also need to add an entry in the Android Manifest RXML. An application can define multiple RemoteWorker services, each running in a separate process.

<! -- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkerService"
    android:exported="false"
    android:process=":background" />
Copy the code

△ Manifest registration service sample code

Here you can see that we have added a new service for the worker process named Background. Now that you have defined the service in RXML, you need to go a step further and specify the component name to bind in the work request.

val inputData = workDataOf(
  ARGUMENT_PACKAGE_NAME to context.packageName,
  ARGUMENT_CLASS_NAME to RemoteWorkerService::class.java.name
)
 
val request = OneTimeWorkRequestBuilder<RemoteDownloadWorker>()
    .setInputData(inputData)
    .build()
 
WorkManager.getInstance(context).enqueue(request)
Copy the code

△ Example code to queue a RemoteWork object

The component name is a combination of the package name and the class name that you need to add to the input data for the work request and then create the work request with this input data so that the WorkManager knows which service to bind to. We queue the work as usual, and when the WorkManager is ready to do the work, it first finds the bound service based on what is defined in the input data and executes the doRemoteWork method. This leaves all the tedious cross-process communication to the WorkManager.

conclusion

WorkManager is the recommended solution for long execution. It is recommended that you use WorkManager to request and cancel long running work tasks. Learn how and when to use the rush work API and how to write reliable, high-performance multi-process applications. Hopefully this article has been helpful, and the next article will give a brief introduction to the new background task checker, so stay tuned!

For more resources, please see:

  • Codelab: Use WorkManager to handle background tasks
  • Codelab: Advanced knowledge of WorkManager
  • WorkManager sample code

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!