Scheduling Works with WorkManager in Android

Photo by Janosch Diggelmann on Unsplash
Photo by Janosch Diggelmann on Unsplash
WorkManager is the officially recommended API for persistent work in the background. The persistent work refers to the work remains scheduled through app restarts or system reboots.

WorkManager is the officially recommended API for persistent work in the background. The persistent work refers to the work remains scheduled through app restarts or system reboots. This article will introduce how to use WorkManager to schedule work.

The complete code for this chapter can be found in .

Background Works

Android provides several ways for developers to perform background works. Before diving further into WorkManager, let’s first understand what are the different background work types. In the figure below, we can divide the background work into three types (please refer to here for details):

  • Immediate: Works that need to be executed immediately and will complete soon.
  • Long-Running: Works that might take some time to completed, possibly longer than 10 minutes.
  • Deferrable: Works that does not require immediate processing. Works that can be scheduled to be processed at a certain point in time. Or works that needs to run periodically.

Among these three types, each can be subdivided into (please refer to here for details):

  • Persistent work: Remain scheduled through app restarts or system reboots. For example, we want to ensure that important data is completely written back to the database, even if the process is forced to end, it will not be interrupted.
  • Impersistent work: No longer scheduled after the process ends.
Types of background work
Types of background work from Android Developers

In summary, there are six types of background works (please refer to here for details):

CategoryPersistentThey persist
ImmediateWorkManagerCoroutines
Long runningWorkManagerNot recommended
DeferrableWorkManagerNot recommended

All persistent work should be implemented with WorkManager. Immediate impersistent works should be implemented using coroutines.

Among them, long running and deferrable impersistent works are marked as not recommended. Because impersistent works should not be long running or deferrable. Long running works take a long time, so it may be interrupted when the process is killed. Deferrable work will be executed at some point later, so it is possible that the app will be restarted before it is executed. So long running and deferrable works should only be considered as persistent works.

WorkManager

WorkManager stores WorkRequest in its database to implement persistent work. So even after the system reboots, WorkManager can get the WorkRequest data from the database and reschedule them.

In addition to WorkManager, Android provides other ways to handle background work. In the article Modern background execution in Android, the author analyzes several use cases of background works, as well as solutions.

Use Cases and Solutions
Use Cases and Solutions from Modern Background Execution in Android

Dependency

First, we add the dependency of WorkManager. WorkManager supports three ways to implement works. Please add the corresponding dependency into your build.gradle according to the way you choose.

The first is pure Java tasks.

dependencies {
    implementation "androidx.work:work-runtime:2.7.0"
}

The second is Kotlin suspend works .

dependencies {
    implementation "androidx.work:work-runtime-ktx:2.7.0"
}

The third is RxJava2 works.

dependencies {
    implementation "androidx.work:work-runtime-rxjava2:2.7.0"
}

In this article, we will only introduce the second one, which is the works of supporting Kotlin suspend.

Creating Works

To create a work, we only need to inherit CoroutineWorker, and implement doWork(). doWork() will be executed by WorkManager in the background and finally returns a Result.

  • If the work is executed successfully, it returns Result.success().
  • If the work is executed unsuccessfully, it returns Result.failure().
  • If the work is executed unsuccessfully, and you want WorkManager to retry the work after a period of time, return Result.retry().

In the code below, we create LogWorker. In the doWork(), we can obtain input data form inputData. In this way, we can pass in different data every time we create a work.

package com.waynestalk.workmanagerexample

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters

class LogWorker(appContext: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(appContext, workerParameters) {
    companion object {
        const val MESSAGE = "message"
    }

    private val tag = javaClass.canonicalName

    override suspend fun doWork(): Result {
        val message = inputData.getString(MESSAGE) ?: return Result.failure()
        Log.d(tag, message)
        return Result.success()
    }
}

Scheduling One-Time Works

OneTimeWorkRequest is used to create a work that is only executed once. We can build it through OneTimeWorkRequestBuilder, then call WorkManager.enqueue() to schedule it.

We can call .setInputData() to pass data to the work, and doWork() can use inputData to obtain the data. In addition, you can set how long the work will start to be executed with .setInitialDelay().

val workRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "This is an one time work"))
    .setInitialDelay(1, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context).enqueue(workRequest)

Scheduling Periodic Tasks

PeriodicWorkRequest is used to create a work that is executed periodically . We can create it through PeriodicWorkRequestBuilder, and specify the interval time and flex time.

  • Interval: Refers to the time of each cycle. It cannot be less than PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS.
  • Flex: Refers to the time that the work will be started at any point in this time and completed. It cannot be less than PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS.
Flexible run intervals
Flexible run intervals
val workRequest = PeriodicWorkRequestBuilder<LogWorker>(
    PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS,
    PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS,
)
    .setInputData(workDataOf(LogWorker.MESSAGE to "This is a periodic work"))
    .setInitialDelay(1, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context).enqueue(workRequest)

Constraints

If Constraints are set, the work will not be executed until the conditions are will met. The conditions that can be set are as follows:

  • NetworkType : When the network status meets the requirements, the work will be executed.
    • CONNECTED
    • METERED
    • NOT_REQUIRED
    • NOT_ROAMING
    • TEMPORARILY_UNMETERED
    • UNMETERED
  • BatteryNotLow: When set true, the work will be executed when the device is not in low battery mode.
  • RequiresCharging: When set true, the work will only be executed when the device is charging.
  • DeviceIdle: When set true, the work will be executed only when the device is idle.
  • StorageNotLow: When set true, the work will be executed only when the storage space of the device is not low.
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()
val workRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "This is an one time work with network constraint"))
    .setConstraints(constraints)
    .build()
WorkManager.getInstance(context).enqueue(workRequest)

Retry

In doWork(), it is possible for some reason unable to perform the work. However, we don’t want to return Result.failure(), but want that WorkManager can retry the work. Then, we can return Result.retry(). In doWork(), you can obtain the number has been retried from runAttemptCount.

package com.waynestalk.workmanagerexample

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters

class RetryLogWork(appContext: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(appContext, workerParameters) {
    companion object {
        const val MESSAGE = "message"
    }

    private val tag = javaClass.canonicalName

    override suspend fun doWork(): Result {
        val message = inputData.getString(MESSAGE) ?: return Result.failure()
        Log.d(tag, "$message, retry count: $runAttemptCount")
        return Result.retry()
    }
}

And when creating a WorkRequest, you also need to set backoff delay and backoff policy.

  • Backoff delay: The waiting time before the first retry. It cannot be less than OneTimeWorkRequest.MIN_BACKOFF_MILLIS.
  • Backoff policy: The way to increase the backoff delay every time the work is going to be retried.
    • LINEAR:Backoff delay * runAttemptCount
    • EXPONENTIAL:Backoff delay * (2 ^ (runAttemptCount – 1))
val workRequest = OneTimeWorkRequestBuilder<RetryLogWork>()
    .setInputData(workDataOf(RetryLogWork.MESSAGE to "This is a retry work"))
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS,
    )
    .build()
WorkManager.getInstance(context).enqueue(workRequest);

Scheduling Unique Works

Sometimes we want to schedule the same work multiple times, but when a work has already been scheduled, we want the new work replace the existing task, or simply ignore the new work. Call WorkManager.enqueueUniqueWork()to schedule uniqueness time work, or call WorkManager.enqueueUniquePeriodicWork()to row periodic work process uniqueness.

ExistingWorkPolicy is the policy that should be taken when there is already a work scheduled.

  • REPLACE: Replace the existing work with new ones.
  • KEEP: Ignore the new work.
  • APPEND: Chain new and existing work. That is, after executing the existing work, execute the new work.
val workRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "This is an unique one time work"))
    .setInitialDelay(5, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context).enqueueUniqueWork(
    "unique_one_time",
    ExistingWorkPolicy.REPLACE,
    workRequest,
)

Chaining Works

Finally, WorkManager allows us to schedule a series of works. You first need to call WorkManager.beginWith(), and then all calls WorkManager.then(). Both of these methods can receive type of WorkRequest or List<WorkRequest>. When List<WorkRequest> is passed in, the works in the list will be executed in parallel, and the next WorkRequest.then() work will not start until all the works in the list are executed.

val workRequest1 = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "Work 1"))
    .build()
val workRequest2 = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "Work 2"))
    .build()
val workRequest3 = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(workDataOf(LogWorker.MESSAGE to "Work 3"))
    .build()
WorkManager.getInstance(context)
    .beginWith(listOf(workRequest1, workRequest2))
    .then(workRequest3)
    .enqueue()
}

Conclusion

WorkManager ensures that the code we want to execute will be executed even if the system restarts, the app restarts, or the app is killed. So when the app needs to store important data, WorkManager provides a perfect solution for that.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like