WorkManager is an Android Jetpack extension library that makes it easy to plan for tasks that can be deferred, asynchronous, but need to run reliably. Using WorkManager is currently the best practice on the Android platform for the vast majority of background execution tasks.

If you’ve been following this series, you’ll have noticed that we’ve already discussed:

  • Android Jetpack WorkManager | Android Chinese teaching video
  • WorkManager practices in Kotlin

This article will introduce:

  • Defining periodic Tasks
  • Cancel the task
  • Custom WorkManager configuration

Repetitive tasks

In previous articles, we introduced using OneTimeWorkRequest to plan tasks. But if you want tasks to be repeated periodically, you can use PeriodicWorkRequest.

Let’s first look at the differences between the two types of WorkRequest:

  • Minimum cycle duration is 15 minutes (same as JobScheduler)
  • The Worker class cannot be chain-executed in PeriodicWorkRequest
  • Prior to V2.1-alpha02, you could not set the initial delay when creating PeriodicWorkRequest

Some of the common problems I encounter in discussions with others have to do with periodic tasks. In this article, I’ll cover the basics of periodic tasks as well as common use cases and errors. I’ll also cover several ways to write tests for the Worker class.

API

The call to create PeriodicWorkRequest is not very different from the previous method of creating one-off tasks, except that an additional parameter is used to specify the minimum repeat interval:

val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS).build()
Copy the code

This parameter is called “minimum interval” because Android’s battery optimization strategy and some of the constraints you add extend the time between iterations. For example, if you specify that a task will only run while the device is charging, then if the device is not charging, even after the minimum interval, the task will not execute until the device starts charging.

PeriodicWorkRequest works with charge status constraints

In this scenario, we need to add a charging constraint to PeriodicWorkRequest and queue it:

val constraints = Constraints.Builder(
                   .setRequiresCharging(true)
                   .build()

val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
                   .setConstraints(constraints)
                   .build()

val workManager = WorkManager.getInstance(context)
workManager.enqueuePeriodicWork(work)
Copy the code

Instructions on how to get a WorkManager instance:

WorkManager V2.1 has deprecated WorkManager#getInstance() in favor of WorkManager#getInstance(context: context). The new approach works in the same way as before, except that it supports new on-demand initialization capabilities. In the rest of the article, I’ll use the new syntax that requires passing in the context to get the WorkManager instance.

A quick note about “minimum intervals” : Since WorkManager needs to balance two different requirements: The application’s WorkRequest and Android systems limit the need for battery consumption, so even if all the constraints you set for WorkRequest are met, your Work can still be executed with some additional latency.

Android includes a set of battery optimization strategies: when the user is not using the device, the system minimizes activity to save battery life. These policies have an impact on task execution: when your device goes into Doze mode, task execution may be delayed until the next maintenance window.

Interval and Elastic interval (FlexInterval)

As mentioned earlier, WorkManager cannot guarantee that a task will be executed at a precise time, but if that is your requirement, you may need to look for other apis. Because the repetition interval is actually the minimum interval, WorkManager provides an additional parameter that you can use to specify a window in which Android can perform your tasks.

In short, you can specify a second interval to control the interval in which you can run your periodic task during a repeated cycle. The position of the second interval (elastic interval) is always at the end of the interval.

Let’s look at an example: Suppose you want to create a periodic task with a repetition period of 30 minutes, you can specify an elastic interval that is smaller than the repetition period, in this case 15 minutes.

Based on the above parameters, the actual code for constructing PeriodicWorkPequest is:

val logBuilder = PeriodicWorkRequestBuilder<MyWorker>(
                         30, TimeUnit.MINUTES, 
                         15, TimeUnit.MINUTES)
Copy the code

As a result, our Worker will execute in the latter part of the cycle (the position of the elastic interval is always at the end of the repeat cycle) :

PeriodicWorkRequest with 30-minute intervals and an elastic 15-minute interval

Remember, these points in time are always based on the constraints contained in the WorkRequest and the state of the device.

About this function, if you want to learn more, can read PeriodicWorkRequest. Builder document.

The daily task

Because periodic intervals are imprecise, you cannot create PeriodicWorkRequest that executes at specified times of the day, even if we relax the precision restrictions.

You can specify a 24-hour cycle, but since the execution of the task is related to Android’s battery optimization strategy, you can only expect the Worker to be executed around the specified time period. So the result might be: your task will be executed at 5:00AM the first day, 5:25AM the second day, 5:15AM the third day, 5:30AM the fourth day, and so on. Over time, errors accumulate.

Currently, if you need to execute a Worker at roughly the same time each day, the best option is to use OneTimeWorkRequest and set the initial delay so that you can execute the task at the correct time:

val currentDate = Calendar.getInstance()
val dueDate = Calendar.getInstance()
 
// Set to execute at approximately 05:00:00 AM
dueDate.set(Calendar.HOUR_OF_DAY, 5)
dueDate.set(Calendar.MINUTE, 0)
dueDate.set(Calendar.SECOND, 0)

if (dueDate.before(currentDate)) {
    dueDate.add(Calendar.HOUR_OF_DAY, 24)}valTimeDiff = duedate. timeInMillis - CurrentDate.timeInmillisval dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker> 
        .setConstraints(constraints) .setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
         .addTag(TAG_OUTPUT) .build()

WorkManager.getInstance(context).enqueue(dailyWorkRequest)
Copy the code

This completes the first execution. Next we need to queue the next task when the current task completes successfully:

class DailyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

  override fun doWork(a): Result {
    val currentDate = Calendar.getInstance()
    val dueDate = Calendar.getInstance()

    // Set to execute at approximately 05:00:00 AM
    dueDate.set(Calendar.HOUR_OF_DAY, 5)
    dueDate.set(Calendar.MINUTE, 0)
    dueDate.set(Calendar.SECOND, 0)

    if (dueDate.before(currentDate)) { 
      dueDate.add(Calendar.HOUR_OF_DAY, 24)}valTimeDiff = duedate. timeInMillis - CurrentDate.timeInmillisval dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker>()
            .setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
            .addTag(TAG_OUTPUT)
            .build()

    WorkManager.getInstance(applicationContext)
            .enqueue(dailyWorkRequest)

	return Result.success()
  }

}
Copy the code

Remember that the actual time to execute the Worker depends on the constraints you use in WorkRequest and the optimization actions for the Android platform.

Status of periodic tasks

As mentioned earlier, one of the differences between periodic tasks and one-off tasks is that task chains cannot be established through PeriodicWorkRequest. This constraint exists because in a task chain, you transition to the next Worker in the task chain when the state of one Worker changes to SUCCEEDED, and PeriodicWorkRequest does not.

The state of the PeriodicWorkRequest

A periodic task does not end in a SUCCEEDED state; it continues to run until it is canceled. When you call Result#success() or Result# Failure () in the Woker of a periodic task, the periodic task returns to the ENQUEUED state and waits for the next execution.

For this reason, you cannot establish a task chain when using periodic tasks, nor can you with UniqueWorkRequest. As a result, PeriodicWorkRequest also loses the ability to APPEND tasks: you can only use KEEP and REPLACE, not APPEND.

Data input and output

WorkManager allows you to pass a Data object to your Worker, while the Success and Failure methods are called, A new Data object is also returned to you (since the Worker execution is stateless when you return Result# Retry (), there is no Data output option).

In a chain composed of one-time workers, the return value of one Worker will become the input value of the next Worker in the chain. We already know that a periodic task cannot use a task chain because it does not end in a “success” state — it is only terminated by a cancel operation.

So, where do we see and use the data returned by the Result#success(outData) method?

We can observe this Data via PeriodicWorkRequest’s WorkInfo. Only before a periodic task is executed next time, we can check its output by determining whether the Worker is in ENQUEUED state:

val myPeriodicWorkRequest =
        PeriodicWorkRequestBuilder<MyPeriodicWorker>(1. TimeUnit.HOURS).build() WorkManager.getInstance(context).enqueue(myPeriodicWorkRequest) WorkManager.getInstance() .getWorkInfoByIdLiveData(myPeriodicWorkRequest.id) .observe(lifecycleOwner, Observer { workInfo ->if((workInfo ! =null) && 
      (workInfo.state == WorkInfo.State.ENQUEEDED)) {
    	val myOutputData = workInfo.outputData.getString(KEY_MY_DATA)
  }
})
Copy the code

If you need the periodic Worker to be able to provide some result data, the above approach may not be your best option. A better option is to transfer the data through another medium, such as database tables.

More about task status information, please refer to the series of the Android Jetpack WorkManager | Android Chinese teaching video, and the WorkManager document: task state and observation mission.

Special task

Some WorkManager use cases can fall into a pattern where tasks are queued as soon as the application starts. These tasks could be background synchronization tasks that you want to perform periodically, or they could be downloads of scheduled content. Whatever it is, the common pattern is that these tasks need to be queued up as soon as the application starts.

I’ve seen this pattern several times, in the Application#onCreate method, where the developer creates and enlists the WorkRequest. Everything seems fine until you notice that some tasks are repeated many times. This is especially true for periodic tasks that do not reach the final state unless cancelled.

We often say that WorkManager will ensure that your tasks are performed even if your application is shut down or your device is restarted. So, trying to queue your Worker every time the app starts will result in a new WorkRequest being added every time the app starts. If you are using OneTimeWorkRequest, this is probably not a problem because WorkRequest also ends once the task completes. But “end” is an entirely different concept for periodic tasks, with the result that you can easily queue multiple periodic tasks repeatedly.

To overcome such a situation, solution is to use WorkManager# enqueueUniquePeriodicWork () will you WorkRequest as a unique task (Work) to chassis to join the queue:

class MyApplication: Application() {

  override fun onCreate(a) {
    super.onCreate()
    val myWork = PeriodicWorkRequestBuilder<MyWorker>(
                         1, TimeUnit.HOURS)
                         .build()

    WorkManager.getInstance(this). EnqueueUniquePeriodicWork (" MyUniqueWorkName ", ExistingPeriodicWorkPolicy. KEEP the myWork)}}Copy the code

This will help you avoid tasks being queued multiple times.

Use KEEP or REPLACE?

Which policy you choose depends on what actions you perform in the Worker. Personally, I usually use the KEEP policy because it’s lighter, doesn’t have to replace existing WorkRequest, and also avoids cancelling already running workers.

I only use REPLACE when I have a good reason, for example, when I want to reschedule a Worker’s own doWork() method.

If you choose to use the REPLACE policy, your Worker should handle the stop state appropriately, because under this policy, if a new WorkRequest is enqueued while the Worker is running, the WorkManager may have to cancel the running instance. However, you should also handle the stop state in any case, because WorkManager may also stop your task if a constraint is no longer met while the Worker is being executed.

For more information about unique tasks, see the documentation: Unique Jobs.

Test periodic task

The WorkManager test documentation is exhaustive and covers basic test scenarios. After The release of WorkManager V2.1, you have two ways to test your Worker:

  • WorkManagerTestInitHelper
  • TestWorkerBuilder and TestListenableWorkerBuilder

Use WorkManagerTestInitHelper, you can test your Worker class analog delay, constraint condition and cycle requirements be met, and so on and so forth. The advantage of this test approach is that it can handle situations where a Worker enlists itself or another Worker class, as seen in the previous example, which implements a “DailyWorker” that runs at about the same time every day. For more information, consult: WorkManager’s Test documentation.

If you need to test CoroutineWorker, RxWorker and ListenableWorker, use WorkManagerTestInitHelper will bring some additional complexity, Because you cannot rely on its SynchronousExecutor.

To test these classes more directly, WorkManager V2.1 adds a new set of WorkRequest constructors:

  • TestWorkerBuilder is used to call the Worker class directly
  • TestListenableWorkerBuilder used for direct call ListenableWorker, RxWorker or CoroutineWorker

The advantage of these new constructors is that you can use them to test any kind of Worker class, because when using them, you can run the corresponding Worker directly.

You can learn more by reading the testing with WorkManager 2.1.0 document or by viewing an example of testing with these new constructors in the Sunflower sample application:

import android.content.Context
import androidx.test.core.app.ApplicationProvider
mport androidx.work.ListenableWorker.Result
import androidx.work.WorkManager
import androidx.work.testing.TestListenableWorkerBuilder
import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
 
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
  private lateinit var context: Context

  @Before
  fun setup(a) {
    context = ApplicationProvider.getApplicationContext()
  }

  @Test
  fun testRefreshMainDataWork(a) {
    / / get ListenableWorker
    val worker = TestListenableWorkerBuilder<SeedDatabaseWorker>(context).build()

    // Execute the task synchronously
    val result = worker.startWork().get()
    assertThat(result, `is`(Result.success()))
  }
}
Copy the code

conclusion

I hope you found this article helpful, and I’d love to hear about how you use WorkManager. If you have a better idea of what WorkManager can do and how to write about it, feel free to contact me on Twitter @pfmaggi.

WorkManager related resources

  • The developer guide | in the WorkManager thread processing
  • Reference guide | androidx. Work
  • Issue log | WorkManager
  • Codelab | use the WorkManager processing background tasks
  • WorkManager source code (part of AOSP)
  • Use the WorkManager speech | (2018) Android developer summit
  • Issue Tracker
  • Stack Overflow’s [Android-workManager] tag
  • A series of articles about Power on the Android Developer blog