The WorkManager API was recently announced at Google I/O. The goal of this library is to help simplify the way we handle background work. Specifically, it lets you create a task and hand it off to WorkManager to run immediately or at a later time.
Previously, we had to rely on multiple APIs to achieve this goal depending on the versions of Android that we needed to support in our apps. Thankfully the team at Evernote saw our frustrations and created the Android-Job library. Depending on the Android version that your user is running, either the JobScheduler
, GcmNetworkManager
or AlarmManager
was being used behind a simple API.
However, with the release of WorkManager, it’s now time to move on. Here’s a direct quote from the README of the Android-Job library:
If you start a new project, you should be using WorkManager instead of this library. You should also start migrating your code from this library to WorkManager. At some point in the future this library will be deprecated.
So let’s see what it takes to make the move when you’ve already developed your app around the Android-Job library.
Adding the dependencies
The first step is to modify your application’s build.gradle
file with the latest dependencies. If you’re using Kotlin inside of your application, there is an optional -ktx
component with a few helper extension functions.
implementation "android.arch.work:work-runtime:$workManagerVersion"
// if you're using Kotlin
implementation "android.arch.work:work-runtime-ktx:$workManagerVersion"
WorkManager vs. Android-Job
The API and feature set of Android-Job and WorkManager are very similar. In order to understand the similarities and differences, we’re going to review the main components of WorkManager and how they are more or less represented in Android-Job.
Analogous classes in Android-Job and WorkManager:
Android-Job | WorkManager |
---|---|
Job |
Worker |
JobCreator |
WorkRequest |
JobManager |
WorkManager |
PersistableBundleCompat |
Data |
Let’s see how these classes are similar across implementations when it comes to Android’s WorkManager
:
Worker
: This is where you put the code for the work you want to perform in the background. It’s analogous to theJob
class from Android-Job.WorkRequest
: This represents a request to do some work. Very similar to theJobRequest
class of Android-Job. However, unlike Android-Job we will need to pass in your Worker as part of creating your WorkRequest. This step eliminates the need for theJobCreator
class that was part of the Android-Job setup.WorkManager
: This class actually schedules your WorkRequest and makes it run. Conceptually the same as theJobManager
class in Android-Job.Data
: This class is a lightweight container for key/value pairs. The same as thePersistableBundleCompat
class from Android-Job. It’s used for handling the small amounts of data that you pass in/out of WorkRequests.
To schedule requests the API is very similar:
Android-Job | WorkManager |
---|---|
schedule(...) |
enqueue(...) |
Example:
Android-Job
JobManager.instance().schedule(SimpleJob.buildRequest())
WorkManager
WorkManager.getInstance().enqueue(SimpleJob.buildRequest())
With that covered, we’ll look at a few sample background jobs. First we’ll see the Android-Job version and then how we can achieve the same thing with WorkManager.
One-time requests
Let’s start with the most basic type, those requests that occur immediately and with no special constraints.
Example:
Android-Job
class SimpleJob : Job() {
override fun onRunJob(params: Params?): Result {
Timber.i("Hello, %s!")
return Result.SUCCESS
}
companion object {
const val JOB_TAG = "simple_job"
@JvmStatic
fun buildRequest(): JobRequest? {
return JobRequest.Builder(JOB_TAG)
.startNow()
.build()
}
}
}
WorkManager
class SimpleJob : Worker() {
override fun doWork(): Worker.Result {
Timber.i("Hello, %s!")
return Worker.Result.SUCCESS
}
companion object {
const val JOB_TAG = "simple_job"
@JvmStatic
fun buildRequest(): WorkRequest? {
return OneTimeWorkRequestBuilder<SimpleJob>()
.addTag(JOB_TAG)
.build()
}
}
}
Here’s a step-by-step listing of what has changed:
- We now extend the
Worker
class - We now override the
doWork()
method instead ofonRunJob()
Worker.Result
replacesJob.Result
as the return value of our background work method- Our
buildRequest()
method returns aWorkRequest
instead of aJobRequest
- Finally we use the
OneTimeWorkRequestBuilder
class to build our request and supply ourJOB_TAG
One-time requests with data
In this scenario we’re going to step it up a notch by dealing with some input data to our job.
Example:
Android-Job
class SimpleJob : Job() {
override fun onRunJob(params: Params?): Result {
val extras = params?.extras
val name = extras?.getString(PARAM_NAME, "nobody")
Timber.i("Hello, %s!", name)
return Result.SUCCESS
}
companion object {
const val JOB_TAG = "demo_job"
const val PARAM_NAME = "param_name"
@JvmStatic
fun buildRequest(name: String): JobRequest? {
val extras = PersistableBundleCompat()
extras.putString(PARAM_NAME, name)
return JobRequest.Builder(JOB_TAG)
.startNow()
.setExtras(extras)
.build()
}
}
}
WorkManager
class SimpleJob : Worker() {
override fun doWork(): Worker.Result {
val name = inputData.getString(PARAM_NAME, "nobody")
Timber.i("Hello, %s!", name)
return Worker.Result.SUCCESS
}
companion object {
const val JOB_TAG = "demo_job"
const val PARAM_NAME = "param_name"
@JvmStatic
fun buildRequest(name: String): WorkRequest? {
val data = mapOf(PARAM_NAME to name).toWorkData()
return OneTimeWorkRequestBuilder<SimpleJob>()
.addTag(JOB_TAG)
.setInputData(data)
.build()
}
}
}
Here’s a step-by-step listing of what has changed:
- By means of an extension method on the
Map
class,toWorkData()
, we create theData
class that is needed to send inputs to our job - Inside of the
doWork()
method we access our job inputs via theinputData
variable
One-time requests with constraints
This time we want to require that there is a netwok connection and that the phone is charging before we execute our job. This only affects our buildRequest()
method.
Example:
Android-Job
@JvmStatic
fun buildRequest(name: String): JobRequest? {
val extras = PersistableBundleCompat()
extras.putString(PARAM_NAME, name)
return JobRequest.Builder(JOB_TAG)
.setExtras(extras)
.setRequiresCharging(true)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.build()
}
WorkManager
@JvmStatic
fun buildRequest(name: String): WorkRequest? {
val data = mapOf(PARAM_NAME to name).toWorkData()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
return OneTimeWorkRequestBuilder<SimpleJob>()
.addTag(JOB_TAG)
.setInputData(data)
.setConstraints(constraints)
.build()
}
Unlike Android-Job’s JobRequest.Builder
class, the WorkRequest.Builder
class uses a separate class, Constraints
, to hold on to the set of desired conditions for executing a job.
Periodic requests
The final example we’ll cover is when you want to perform some piece of work on a repeating basis. Let’s say you need to take an action every hour in the background.
Example:
Android-Job
@JvmStatic
fun buildRequest(name: String): JobRequest? {
val extras = PersistableBundleCompat()
extras.putString(PARAM_NAME, name)
return JobRequest.Builder(JOB_TAG)
.setPeriodic(TimeUnit.HOURS.toMillis(1))
.setExtras(extras)
.setRequiresCharging(true)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.build()
}
WorkManager
@JvmStatic
fun buildRequest(name: String): WorkRequest? {
val data = mapOf(PARAM_NAME to name).toWorkData()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
return PeriodicWorkRequestBuilder<SimpleJob>(1, TimeUnit.HOURS)
.addTag(JOB_TAG)
.setInputData(data)
.setConstraints(constraints)
.build()
}
Notice that we now have an entirely new type of WorkRequest
, the PeriodicWorkRequest
class. This class is used to handle our repeating work. This differs from Android-Job where we were relying on a field of the JobRequest.Builder
to specify that a request should be periodic.
All-in-all, the move from Android-Job to WorkManager is very seamless. It’s nice that the concepts are similar and even the naming is almost identical.
Hope you found this helpful. Happy coding!