Scheduling tasks with WorkManager

Clint Paul
May 8 · 7 min read
Photo by Moritz Kindler on Unsplash

There is a simple way to describe the features of WorkManager by splitting its name. Imagine you are running a company, and you are responsible for handling tons of things at once. At some point, it will be difficult for you to conduct it all by yourself. Let’s call these tasks “Work.” Naturally, you will delegate these “Works” to your subordinates or your employees. They are capable of administering it without bothering you. They can complete these tasks with the same efficiency as you could and at the same time reduce the load on your shoulders. Let’s call them “Managers.” So Work+Manager = WorkManager.

Let’s rethink the same scenario in the Android world. Using the WorkManager API, you can schedule tasks that are deferrable or asynchronous. WorkManager will make sure these tasks will run even if your app is exited or is in the background. It is the successor of the Android background scheduling APIs, such as FirebaseJobDispatcher, GcmNetworkManager, and JobScheduler. You can say that the WorkManager is a beast that combines the power of all its predecessors. Also, it is backward compatible ( API Level 14 ) and conscious of battery life. There is another cool feature called “Constraints.” We will learn about it soon.

Under the hood

Image credits: Android developers

When you send a work request, WorkManager will save it in the local database. That’s how it remembers the work even though the app is exited or in the background. Then, it will check whether you are using API level 23+ if, then, it will send the work request to the JobScheduler. If your API level is between 14–22, it will check whether your device has Google play services installed or not. If it is, then GcmNetworkManager will be called. Else, Custom AlarmManager and BroadcastReceiver.

What we are going to build?

Define the Work

class ImageDownloadWorker(context: Context, params: WorkerParameters): CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
return try {
downloadImage()
Result.success(outputData)
}catch (e: Exception) {
Result.failure()
}

}
}

We can create a Worker class by simply extending any class by a Worker class. It will override a method called doWork() that will run asynchronously on a background thread provided by the WorkManager. You might have noticed that we are extending a CoroutineWorker instead of a normalWorker class. If you are using Java, then you can use ListenableWorker . The reason is that the Worker class runs synchronously in the background, whereas ListenableWorker and CoroutineWorker will run asynchronously.

The Result will notify you whether your work succeeded, failed, or needs to be retried.

Creating a WorkRequest

Creating a One-Time WorkRequest

Create a new instance of the WorkManager class.

private val workManager by lazy {
WorkManager.getInstance(applicationContext)
}

Next, create your WorkRequest


val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val imageDownloadWorker = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
.setConstraints(constraints)
.addTag(UNIQUE_TAG)
.build()

workManager.enqueueUniqueWork(
"oneTimeImageDownloader",
ExistingWorkPolicy.KEEP,
imageDownloadWorker
)

Let’s go through each line of code and understand what’s happening.

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

As the name suggests, Constraints will limit or restrict the operations of the WorkManager if certain conditions are not satisfied. Here, I make sure that the WorkManager will run only if there is a working internet connection. Like that, you can set a few other constraints such as,

  • The device battery is good
  • Device storage is good
val imageDownloadWorker = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
.setConstraints(constraints)
.addTag(UNIQUE_TAG)
.build()

Create an OneTimeWorkRequestBuilder and add the constraint details and a unique TAG. We can use this TAG to cancel or observe our work.

workManager.enqueueUniqueWork(
"oneTimeImageDownloader",
ExistingWorkPolicy.KEEP,
imageDownloadWorker
)

Finally , enqueue the WorkRequest.

Creating a periodic WorkRequest

Sometimes, our apps need to run some tasks periodically. Like, syncing a day’s activities, or backup the data like WhatsApp does. In that case, you can use PeriodicWorkRequest.

val builder = Data.Builder()
builder.putString(KEY_IMAGE_ID, "1")
val data = builder.build()

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val imageDownloadWorker = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
15, TimeUnit.MINUTES
).setInputData(data)
.setConstraints(constraints)
.addTag(UNIQUE_TAG)
.build()

workManager.enqueueUniquePeriodicWork(
"periodicImageDownloader",
ExistingPeriodicWorkPolicy.KEEP,
imageDownloadWorker)

The difference between an OneTimeWorkRequest and the PeriodicWorkRequest is that we have an additional parameter that specifies the minimum interval. You might be thinking that why it is called a minimum interval and not a repeating interval? The reason is that the interval can get extended due to the constraints which we have set earlier. If there is no internet connection, then the work will not run, and this interval can get extended. Also, keep in mind that the minimum period length is 15 minutes.

Adding Input and Output

We use Data objects for this purpose. They are lightweight containers for key/value pairs. They help us to transfer data in and out from WorkRequest.

val builder = Data.Builder()
builder.putString(KEY_IMAGE_ID, "1")
val data = builder.build()
  • Create a Data builder object
  • Then, add the ImageId to the builder using the putString method. KEY_IMAGE_ID from constants as the ‘Key’ and “1” as the value.
  • Call build on the Data.Builder object to make your data object and return it.

Complete code

val builder = Data.Builder()
builder.putString(KEY_IMAGE_ID, "1")
val data = builder.build()

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val imageDownloadWorker = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
.setConstraints(constraints)
.setInputData(data)
.addTag(UNIQUE_TAG)
.build()

workManager.enqueueUniqueWork(
"oneTimeImageDownloader",
ExistingWorkPolicy.KEEP,
imageDownloadWorker
)

Next, let’s retrieve the ImageId in the Worker class.

override suspend fun doWork(): Result {
return try {
val imageId = inputData.getString(KEY_IMAGE_ID)
val picture = downloadImage(imageId)
val outputData = workDataOf(KEY_IMAGE_ID to picture)
Result.success(outputData)
}catch (e: Exception) {
Log.e("doWorkException", e.printStackTrace().toString())
Result.failure()
}

}
  • We can fetch the imageId we passed to the Worker class using the inputData method. Since we passed the data using the putString method, we should use getString to retrieve it.
  • Likewise, we can return any data from the WorkRequest . Here, we are going to return the imageUrl from the API. Create a new data object, just like we did for the input, and use the same key, ‘KEY_IMAGE_ID’ and use ‘picture’ as the value.
  • Return the outputData to the WorkManager using Result.success(outputData)

Complete code

class ImageDownloadWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
return try {
val imageId = inputData.getString(KEY_IMAGE_ID)
val picture = downloadImage(imageId)
val outputData = workDataOf(KEY_IMAGE_ID to picture)
Result.success(outputData)
}catch (e: Exception) {
Log.e("doWorkException", e.printStackTrace().toString())
Result.failure()
}

}

private suspend fun downloadImage(imageId: String?): String? {

val service = ServiceGenerator.createService(Service::class.java)
var imageUrl: String? = null
if (imageId != null) {
val response = service.getImage(imageId)
if (response.isSuccessful) {
response.body().let {

imageUrl = response.body()?.download_url
}
}
}
return imageUrl
}
}

Observing work progress

We can observe the work progress and update our UI according to the result.

private fun observeWork(id: UUID) {
workManager.getWorkInfoByIdLiveData(id).observe(this, { info ->
if (info != null && info.state.isFinished) {
val imageUrl = info.outputData.getString(KEY_IMAGE_ID)
Log.e("imageUrlSuccess", imageUrl.toString())
if (imageUrl != null) {
binding.button.visibility = View.GONE
binding.imageView.visibility = View.VISIBLE
setImage(imageUrl)
}
}
})
}

You can easily observe the work using the worker id and then get the information as a LiveData. We will be able to check if the work is finished. If then we can try retrieving the output of the WorkRequest from the Worker class.

Note: if you are executing a PeriodicWorkRequest, then you can’t check if the state is finished. The reason is that chain of works is not possible in a PeriodicWorkRequest. In a chain of works, one request ends after a SUCCESS and then moves on to the next worker. Since the chain of works is not possible, the PeriodicWorkRequest will be in ENQUEUED status and will be waiting for the next execution.

Image credits: Android developers blog

I hope you were able to understand the working of the WorkManager and its many helpful features. There are many other features of WorkManager I didn’t mention in this article. If you are curious to know more about it, I suggest you check this documentation by the Android developers. Also, you can get the complete code in my GitHub repository.

I hope you and your family are safe during this pandemic. Let’s stay strong together and help each other.

Article posted originally here.

CodeX

Everything connected with Tech & Code

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store