Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

#Scheduling work with WorkManager


WorkManager is an Android Jetpack library that enqueues deferrable background work and guarantees their execution when its constraints are met.

This is the second blog post of the WorkManager series.

Previously on WorkManager...
We discussed what is WorkManager and when can we use it. We understood the terms - guaranteed execution and deferrable work with examples. We had a look into the use cases of WorkManager and what features does it contain. If you want to read on that, do check out the previous blog of the series.

In this blog, we will cover the implementation part of WorkManager.

  • Adding the dependency
  • Classes of WorkManager
  • Scheduling work using OneTimeWorkRequest, PeriodicWorkRequest and observing work information
  • Sending and receiving data
  • Chaining of work requests
  • Setting constraints on Work
  • Cancel Work

##Adding the dependency

In the app/build.gradle file, add

dependencies {

	implementation "androidx.work:work-runtime-ktx:2.4.0"
}

The code snippets in this blog are in Kotlin. The version of the library may differ from the latest one. At the time of writing this blog, the recent version is 2.4.0. Get the latest version of WorkManager library from here.

##Classes

This API comprises of four classes

  1. Worker - The work that needs to be done is defined here.
  2. WorkRequest - The worker class that is going to be executed is defined here.
  • OneTimeWorkRequest
  • PeriodicWorkRequest
  1. WorkManager - It enqueues and manages the work requests.
  2. WorkInfo - It contains information about the work.

##Scheduling work

Let's work on a simple implementation of WorkManager. Let's define a worker called NotificationWorker which extends Worker class. It has a showNotification() method that gets called within doWork() method.

In the activity, let's create a button and a textview. On click of the button, WorkManager will enqueue the WorkRequest and the status of the work will be displayed in the textview.

Step -1: Define the work.

Create a class extending the Worker class and override doWork() method.

class NotificationWorker(context: Context, workerParams: WorkerParameters) : 
Worker(context, workerParams) {
    override fun doWork(): Result {
        
    }
}

Whatever work that needs to be done in this Worker class, we do it inside the doWork() method. Let's call a method named showNotification().

override fun doWork(): Result {
    showNotification("Test Title", "Test Content")
    return Result.success()
}

Three values can be returned - Result.success(), Result.failure() or Result.retry(). We can call any of these options to indicate what happened in the background work.

Define the showNotification() method. It takes two input - title and content, build a notification and show it.

private fun showNotification(title : String, content : String) {
    val notificationManager: NotificationManager =
        applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
        val notificationChannel : NotificationChannel =
            NotificationChannel("a1", "Work Manager", NotificationManager.IMPORTANCE_DEFAULT)
        notificationManager.createNotificationChannel(notificationChannel)
    }

    val notificationBuilder : NotificationCompat.Builder =
        NotificationCompat.Builder(applicationContext, "a1")
            .setContentTitle(title)
            .setContentText(content)

    val notification : Notification = notificationBuilder.build()

    notificationManager.notify(1, notification)
}

Step -2: Create a WorkRequest.

In the activity class, create a OneTimeWorkRequest using

val request : OneTimeWorkRequest = 
				OneTimeWorkRequest.Builder(NotificationWorker::class.java)
				.build()

Step -3: Using WorkManager, enqueue the work.

workManager = WorkManager.getInstance(applicationContext)
workManager.enqueue(request)

Step -4: Use getWorkInfoByIdLiveData() to observe the change in the work information and display in the UI.

workManager.getWorkInfoByIdLiveData(request.id)
            .observe(this, Observer {workInfo ->
                if(workInfo != null) {
                    val status: String = workInfo.state.name
                    status_textview.append(status + "\n")
                }
            })

Let's say we want some work to be repeated, say every 1 hour, we have to build a PeriodicWorkRequest and enqueue it. To make a periodicWorkRequest, use

val request : PeriodicWorkRequest = 
				PeriodicWorkRequest.Builder(NotificationWorker::class.java, 1, TimeUnit.HOURS)
				.build()

##Sending and receiving data

  1. Send Data from Activity and receive data in the Worker class

In the activity, to send data to Worker class, we create a data object and pass with the WorkRequest using method setInputData().

val data : Data = Data.Builder()
            .putString(KEY_WORK_CONTENT, "I am the content of notification")
            .putString(KEY_WORK_TITLE, "Work Manager Test")
            .build()
val request : OneTimeWorkRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
            .setInputData(data)
            .build()

In the Worker class, we use inputData to fetch values passed by Activity/Fragment.

val title : String = inputData.getString(MainActivity.KEY_WORK_TITLE).toString()
val content : String = inputData.getString(MainActivity.KEY_WORK_CONTENT).toString()
  1. Send data from Worker class and receive in the activity using getWorkInfoByIdLiveData() method.

The same way, we create a data object and pass with the Result in the doWork() method.

val data : Data = Data.Builder()
            .putString(KEY_OUTPUT_TEXT, "Work finished!")
            .build()
return Result.success(data)

To receive it in the activity, we observe getWorkInfoByIdLiveData and use outputData of workInfo to get the information passed.

if(workInfo.state.isFinished) {
    status_textview.append("\n" + 
    workInfo.outputData.getString(NotificationWorker.KEY_OUTPUT_TEXT) + "\n")
}

Data object is a set of key/value pairs where keys are Strings and values can be Strings, primitive types or arrays. There is also a MAX_DATA_BYTES limit on the serialized size of the payloads.

##Chaining Requests

WorkManager lets you chain WorkRequests parallely and sequentially. Chaining comes with benefits of passing output of one WorkRequest as input of another WorkRequest.

workManager = WorkManager.getInstance(applicationContext)
workManager
    .beginWith(Arrays.asList(workRequest1, workRequest2))
    .then(workRequest3)
    .then(workRequest4)
    .enqueue()

In the snippet, the WorkManager will begin the work with workRequest1 and workRequest2 running parallely. workRequest3 will start only when both workRequest1 and workRequest2 finishes, followed by workRequest4.

To receive the input in workRequest3 from the parallel execution of workRequest1 and workRequest2, we can use an InputMerger.

##Setting constraints on Work

We can set constraints in the WorkRequest. The work gets executed when its constraints are met. Let's say we want our work to be performed only when the battery is not low, then we can build the constraints as

val constraints : Constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
            .build()

and set it with request as

val request : OneTimeWorkRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
            .setInputData(data)
            .setConstraints(constraints)
            .build()

There are various methods available for constraints as well.

setRequiredNetworkType - It sets a particular network type as a requirement for the work to be done. For example, if we want some task to be executed only when we have a network connection available, then we use network type CONNECTED as a constraint. Other options for network type are METERED, NOT_ROAMING, UNMETERED. Default is NOT_REQUIRED.

setRequiresBatteryNotLow - If set true, the device should have an acceptable battery level for the work to be executed.

setRequiresCharging - If set true, the device should be charging for the work to be executed.

setRequiresDeviceIdle - If set true, the device should be in the idle state for the work to be executed.

setRequiresStorageNotLow - If set true, the work execution requires the available storage to be above a critical threshold.

Read about other available methods in Constraints.Builder.

##Cancellng Work

We can cancel the work using the id, tag or unique chain name.

workManager.cancelWorkById(id)

Methods available to cancel a work are - cancelAllWork, cancelAllWorkByTag, cancelUniqueWork and cancelWorkById.

##Summary

In this blog, we have seen simple code snippets to schedule tasks using WorkManager. After following this blog, you can -

  • Implement your Worker classes.
  • Send and receive data from Worker class.
  • Build your WorkRequests and set constraints on when to execute the work.
  • Enqueue the WorkRequests - OneTimeWorkRequest, PeriodicWorkRequest, chain them parallelly or sequentially using WorkManager.
  • Observe the WorkInfo using getWorkInfoByIdLiveData.
  • Cancel the work, if required.

In this blog, I have used the code snippets. Refer to the simple implementation of the above here.

##References


*If you like this article and found it useful, please clap👏 and help others find this article. If you have any suggestion, let me know!*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment