Fire Off Tasks On Android With WorkManager

A Prelude to WorkManager

If you’ve been an Android developer for a while, you‘ll be familiar with the difficulties faced when trying to write code for scheduling tasks in Android.

Up till now, you might have been using the following APIs for scheduling tasks:

All of these APIs have their own use-case scenarios, where they would be the right fit to get the job done. These APIs have been used by Android developers for years now.

While these APIs are great and have received constant development updates throughout the years, Google has introduced a new API called WorkManager, that does all of this in a way that's much more efficient.

What Is WorkManager?

WorkManager is an API introduced and developed by Google that lets you perform asynchronous, periodic and scheduled tasks on Android in a much more efficient way, compared to the traditional ways.

Android developers know the pain of detecting the user's device and then choosing between JobScheduler, Firebase JobDispatcher, ScheduledExecutorService, or AlarmManager to schedule a task.

WorkManager gets rid of this pain point. It does all the heavy-lifting for you by performing your task exactly the way you desire it to be performed by automatically choosing one of the following three APIs:

  • JobScheduler
  • Firebase JobDispatcher
  • AlarmManager

To just give you an idea, WorkManager is fit for apps that require you to:

  • Fetch data periodically. Example: Weather apps
  • Do an action at a particular time. Example: Reminder based-apps
  • Upload data to a server periodically.
  • Perform a task on the basis of the device info. Example: Battery state

...and more!

WorkManager is part of the Android Architecture Components, which is a part of Android Jetpack. Android Jetpack is a collection of Android components by Google that makes app development easier and the apps more robust.

WorkManager Venn Diagram.jpg

Note : It’s important to keep in mind that while WorkManager is a great solution to perform tasks in Android, it’s also still in the alpha stages.

This article will serve as a tutorial for anyone who’s looking to get started with using the WorkManager API in their Android app.

Prerequisites

In terms of Android knowledge, you’ll need a basic understanding of:

  • Kotlin. You can also use Java but this tutorial will be in Kotlin;
  • How scheduling in Android works;
  • Restrictions for specific APIs, and
  • LiveData from the Android Architecture Components.

For your setup, you’ll need to have:

  • Android Studio 3.2 or above running on your system;
  • Kotlin 1.2 or above;
  • Android SDK Platform 28 or above;
  • Android SDK Build-Tools 28.0.0 or above, and
  • The compileSdkVersion in your app should be set to 28 or above.

Exploring WorkManager’s components…

The WorkManager API has 3 main components:

  1. Worker
  2. WorkRequest
  3. WorkManager

Let’s explore these components further.

Worker

WorkManager Worker.jpg

This is the component of the WorkManager API that actually performs the work. It is responsible for specifying what work needs to be done. You need to override the doWork() method and your long-running code goes inside this method.

The doWork() method performs the work on a background thread. The doWork() method returns a Result, which is an enum with the following values:

  • Result.SUCCESS – The work was performed successfully.
  • Result.FAILURE – The work failed permanently; it won’t be retried.
  • Result.RETRY – The work failed but needs to be retried according to the specified backoff policy.

WorkRequest

WorkManager WorkRequest.jpg

A WorkRequest is responsible for specifying which Worker should run. It takes in a Worker and performs the task specified in the Worker.

There are two types of WorkRequests:

  • OneTimeWorkRequest – For one-time tasks, i.e. non-repeating tasks.
  • PeriodicWorkRequest – For repeating tasks.

Both these requests are built with one of the subclasses of WorkRequest.Builder, which is either OneTimeRequestBuilder or PeriodicRequestBuilder.

When a WorkRequest is created, an ID associated with the request is also created, which is a of type UUID. You can also set a tag for your WorkRequest if you want to access it later via the tag, and a backoff policy in case you want to reschedule the work.

There are two more significant things that you might want to use in your WorkRequest:

Constraints

The Constraints.Builder helper class lets you specify certain criteria which makes sure that your Worker only runs if these criteria are met. Some of these criteria include:

  • setRequiresCharging(boolean requiresCharging) – set to true if you want to run your Worker only when the device is charging.
  • setRequiresDeviceIdle(boolean requiresDeviceIdle) – set to true if you want to run your Worker only when the device is idle.
  • setRequiresBatteryNotLow(boolean requiresBatteryNotLow) – set to true if you don’t want to run your Worker when the device’s battery is low.
  • setRequiresStorageNotLow(boolean requiresStorageNotLow) – set to true if you don’t want to run your Worker when the device’s storage is low.
  • setRequiredNetworkType(NetworkType networkType) – sets the NetworkType with which the Worker should run.
  • addContentUriTrigger(Uri uri, boolean triggerForDescendants) – runs the Worker when a content Uri is updated. If triggerForDescendants is true, the criteria is matched for its descendants too.

Data

The Data.Builder helper class lets you provide data to your Worker.

Please keep in mind that this should not exceed 10KB.You will get an IllegalStateException if you pass data that exceeds this limit.

Think of Data as a key-value pairs store, not unlike SharedPreferences. You can find out more about it here.

WorkManager

The self-titled WorkManager class lets you manage, schedule, and even cancel WorkRequests in your app. It guarantees the work specified in your WorkRequest is executed with the Data provided to it once the Constraints are met.

It also lets you observe the state of the WorkRequest via the WorkStatus class. This is a simple class that is wrapped around with LiveData by the WorkManager, containing information regarding the state of the task being performed.

Depending on your device’s API, WorkManager uses the following techniques to run your WorkRequest:

  • API 14 to 22: If the optional WorkManager-related FirebaseJobDispatcher dependency is added to the project, it uses the FirebaseJobDispatcher itself. Otherwise, it uses a combination of AlarmManager and BroadcastReceiver, which is a custom implementation written in the API.
  • API 23 and above: It uses the JobScheduler API for devices with API 23 and above.

Adding WorkManager to your project

Currently, the WorkManager API depends on Support Library 27.1, but future releases of the API will depend on the new semantic-version-based AndroidX library, introduced by Google in May, 2018.

It’s interesting to note that all the classes in the WorkManager API already reside in the androidx.work package. This means that while the API depends on the traditional Android Support Library, the WorkManager API and all the code pertaining to the API itself is a part of the AndroidX library.

Adding the WorkManager API to your app is simple. You just need to add the following dependencies to your app-level build.gradle file and you’re all set:

dependencies {
    // WorkManager core dependency
    implementation "android.arch.work:work-runtime-ktx:1.0.0-alpha10"
    // Firebase JobDispatcher support
    implementation "android.arch.work:work-firebase:1.0.0-alpha10"
    // Test helpers
    androidTestImplementation "android.arch.work:work-testing:1.0.0-alpha10”
}

Creating a Worker

For the sake of this tutorial, we shall assume that the long running task we need to run via our WorkManager API is backing up data to the cloud. This is done via our backupData() method.

We’re gonna name our Worker as BackupWorker, and run our backupData() method inside the doWork() method after overriding it.

class BackupWorker(context : Context, params : WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {

        // Perform your long-running backup operation here.
        backupData()

        // Return SUCCESS if the task was successful.
        // Return FAILURE if the task failed and you don’t want to retry it.
        // Return RETRY if the task failed and you want to retry it.
        return Result.SUCCESS
    }
}

Building your WorkRequest

Let’s build a OneTimeRequest with the OneTimeRequestBuilder and pass our BackupWorker to it as followed:

val backupWorkRequest = OneTimeWorkRequestBuilder<BackupWorker>().build()

That’s how easy it is to build a OneTimeRequest with WorkManager! Now let’s add some Constraints, Data that has the user’s name, and a tag. It’s ideal if we let the device backup data only when it’s charging and if storage isn’t low. So let’s see how to do this and create some constraints:

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

val data = Data.Builder()
        .putString(“DATA_USER_FIRSTNAME”, “John”)	
        .build()

val backupWorkRequest = OneTimeWorkRequestBuilder<BackupWorker>()
                    .setConstraints(constraints)
                    .setInputData(data)
                    .addTag(“BACKUP_TAG”)
                    .build()

Managing your WorkRequest with WorkManager

Running your first WorkRequest

Executing your WorkRequest with WorkManager is easy. You just need to get an instance of WorkManager and enqueue the request. Here’s how that can be done:

val backupWorkRequest = OneTimeWorkRequestBuilder<BackupWorker>()
                    .setConstraints(constraints)
                    .build()

WorkManager.getInstance().enqueue(backupWorkRequest)

Observing the state of the WorkRequest

Like we mentioned above, the WorkManager API lets you observe the State of your WorkRequest. There are enum values in the State class:

  • BLOCKED – Request is awaiting to be executed.
  • ENQUEUED – Request is enqueued for execution.
  • RUNNING – Request is currently being executed.
  • SUCCEEDED – Request completed successfully.
  • CANCELLED – Request has been cancelled.
  • FAILED – Request has failed.

We can get the ID of a WorkRequest with the getStatusById() method and call the observe() method to observe the state of the request. Let’s see how to do this:

WorkManager.getInstance()
    .getStatusById(backupWorkRequest.id)
    .observe(lifecycleOwner, Observer { workStatus ->
        if (workStatus != null && workStatus.state.isFinished) {
            // Do something here if request has finished executing.
        }
    })

Canceling a WorkRequest

You can cancel a WorkRequest by calling the cancelWorkById() method or even the cancelAllWorkByTag() method if you want to cancel it by passing the tag of the WorkRequest.

WorkManager.getInstance().cancelWorkById(backupWorkRequest.getId())

It’s noteworthy to mention that your WorkRequest may not be canceled by the WorkManager if it is already running or has finished.

Where can you go from here?

Periodic tasks

We have covered the usage of OneTimeWorkRequest in this tutorial, but you can also set up recurring tasks with the PeriodicWorkRequest class. The following code snippet sets up a PeriodicWorkRequest to run every 24 hours:

val periodicWorkRequest = PeriodicWorkRequestBuilder<BackupWorker>(24, TimeUnit.HOURS)

You can read more about how to build PeriodicWorkRequest here.

More advanced usages

There are more advanced usages of the WorkManager, but they aren’t covered in this article for brevity purposes. You can fine-tune WorkManager and leverage advanced features provided by the API, like:

  • Unique sequence of tasks, to run only one instance of a specific set of tasks;
  • Chained sequence of tasks, to run tasks in a specified order;
  • Tasks that take input data and return output data.

You can learn more on how to do this in the official documentation here.

Conclusion

While the WorkManager API is still in alpha stages, it is a great choice to schedule and perform tasks in the background in your Android app. It could be the ideal API to use because of its ease-of-use and regular API updates, thanks to Google making it a part of the Android Architecture Components.

It’s probably not recommended to be used in production as of now, but do go ahead and play around with it in a testing environment!

Last updated on Dec 10, 2018