Getting started with WorkManager [Example]

WorkManager is one of the Android Architecture Components and part of Android Jetpack, a new and opinionated take on how to build modern Android applications. WorkManager is an Android library that runs deferrable background work when the work’s constraints are satisfied.

Earlier we had AlarmManager, JobScheduler, FirebaseJobDispatcher for scheduling the background tasks. But the issues were

  • JobScheduler – Available only for API >= 21
  • FirebaseJobDispatcher – For backward compatibility

So developer had to understand which method to use when. To overcome these issues we have WorkManager, and it will automatically choose the best method for your task and you do not need to write the logic for it. So basically WorkManager is providing an abstraction layer. It gives us a clean interface hiding all the complexities and giving the guaranteed execution of the task.

To get started using WorkManager, first import the library into your Android project.

Add the following dependencies to your app’s build.gradle file:

dependencies {
    def work_version = "2.5.0"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

WorkManager Features

  • Support for both asynchronous one-off and periodic tasks
  • Support for constraints such as network conditions, storage space, and charging status
  • Chaining of complex work requests, including running work in parallel
  • Output from one work request used as input for the next
  • Handles API level compatibility back to API level 14 (see note)
  • Works with or without Google Play services
  • Follows system health best practices
  • LiveData support to easily display work request state in UI

Schedule tasks with WorkManager

WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. The WorkManager API is a suitable and recommended replacement for all previous Android background scheduling APIs, including FirebaseJobDispatcherGcmNetworkManager, and Job Scheduler. WorkManager incorporates the features of its predecessors in a modern, consistent API that works back to API level 14 while also being conscious of battery life.

Important classes in WorkManager

  • Worker: The main class where we will put the work that needs to be done.
  • WorkRequest: defines an individual task, like it will define which worker class should execute the task.
  • WorkManager: The class used to enqueue the work requests.
  • WorkInfo: The class contains information about the works. For each WorkRequest, we can get a LiveData using WorkManager. The LiveData holds the WorkInfo and by observing it we can determine the Work Informations.

Creating Your WorkRequest

To create workrequest, first we need to define the worker class.

Define the work

Work is defined using the Worker class. The doWork() method runs asynchronously on a background thread provided by WorkManager.

To create some work for WorkManager to run, extend the Worker class and override the doWork() method. For example, to create a Worker that download images, you can do the following:

class DownloadWorker(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {

    override fun doWork(): Result {
        //Download Image
        downloadImage()
        return Result.success(outputData)
    }
}

The Result returned from doWork() informs the WorkManager service whether the work succeeded and, in the case of failure, whether or not the work should be retried.

Result.success(): The work finished successfully.
Result.failure(): The work failed.
Result.retry(): The work failed and should be tried at another time according to its retry policy.

Creating a One-Time WorkRequest

OneTimeWorkRequest is Used when we want to perform the work only once.

        val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java).build()

Finally, you need to submit your WorkRequest to WorkManager using the enqueue() method.

            WorkManager.getInstance(this).enqueue(oneTimeWorkRequest)

Creating a Periodic WorkRequest

Occasionally, your work needs to run several times, such as daily backups of a messaging app. In such cases, you use a PeriodicWorkRequest to create your WorkRequest.

        val periodicWorkRequest = PeriodicWorkRequest.Builder(DownloadWorker::class.java, 10, TimeUnit.HOURS).build()

Finally, you need to submit your PeriodicWorkRequest to WorkManager using the enqueue() method.

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
                    "periodicImageDownload",
                    ExistingPeriodicWorkPolicy.KEEP,
                    periodicWorkRequest
            )

In PeriodicWorkRequest,

  1. Use PeriodicWorkRequestBuilder to define your work. Notice that it takes time as a parameter. A restriction requires the interval between successive executions of your work to be at least 15 minutes.
  2. Submit the work to WorkManager by calling enqueueUniquePeriodicWork. You need to pass the uniqueWorkName, existingPeriodicWorkPolicy, and the imageWorker itself.

Creating a Delayed WorkRequest

A delayed WorkRequest is a OneTime WorkRequest whose execution is delayed by a given duration.

val delayedWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setInitialDelay(20, TimeUnit.SECONDS)
                .build()

you need to submit your WorkRequest to WorkManager using the enqueue() method.

WorkManager.getInstance(this).enqueueUniqueWork(
                    "delayedImageDownload",
                    ExistingWorkPolicy.KEEP,
                    delayedWorkRequest
            )

Sending And Receiving Data to/from WorkManager

We can also pass to data to our WorkManager class and we can also get back some data after finishing the work. So let’s see how we can do this.

Sending Data

To send the data to worker, we need to create Data.Builder() and add the data into workRequest using setInputData() .

val data: Data = Data.Builder().putString("task", "The task data").build()
        val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setInputData(data)
                .build()

Then, In your Worker we need to use getInputData() to get data inside the doWork().

override fun doWork(): Result {
        //get data
        val taskDesc = inputData.getString("task")
        return Result.success()
    }

Receiving Data

For receiving we can again use the same concept inside doWork() method.

override fun doWork(): Result {
        val outputData = workDataOf("task" to "task details")
        return Result.success(outputData)
    }

And we can receive this data inside the observer in MainActivity.

val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setInputData(data)
                .build()

        WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneTimeWorkRequest.id).observe(this, { workInfo ->
            val task = workInfo?.outputData?.getString("task")
        })

Features of WorkManager

Work Constraints

Let’s add some constraints in our work so that it will execute at a specific time. We have many constraints available for example.

  • setRequiresCharging(boolean b): If it is set to true the work will be only done when the device is charging.
  • setRequiresBatteryNotLow(boolean b): Work will be done only when the battery of the device is not low.
  • setRequiresDeviceIdle(boolean b): Work will be done only when the device is idle.
val constraints: Constraints = Constraints.Builder().setRequiresCharging(true)
                .build()
        val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setConstraints(constraints)
                .build()

Now after doing this change if you will run your application then the work will only be executed if the device is charging.

Flexible Retry Policy

If you require that WorkManager retry your work, you can return Result.retry() it from your worker. Your work is then rescheduled according to a backoff delay and backoff policy.

  • Backoff delay specifies the minimum amount of time to wait before retrying your work after the first attempt. This value can be no less than 10 seconds (or MIN_BACKOFF_MILLIS).
  • Backoff policy defines how the backoff delay should increase over time for subsequent retry attempts. WorkManager supports 2 backoff policies, LINEAR and EXPONENTIAL.

Every work request has a backoff policy and backoff delay. The default policy is EXPONENTIAL with a delay of 10 seconds, but you can override this in your work request configuration.

Here is an example of customizing the backoff delay and policy.

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

In this example, the minimum backoff delay is set to the minimum allowed value, 10 seconds. Since the policy is LINEAR the retry interval will increase by approximately 10 seconds with each new attempt. For instance, the first run finishing with Result.retry() will be attempted again after 10 seconds, followed by 20, 30, 40, and so on, if the work continues to return Result.retry() after subsequent attempts. If the backoff policy were set to EXPONENTIAL, the retry duration sequence would be closer to 20, 40, 80, and so on.

Work Chaining

You can make works chain for sequential works. Imagine you fetch the image from the network and blur that to image in the application. We can chain the work for sequential processes or executing some works parallelly.

val constraints: Constraints = Constraints.Builder().setRequiresCharging(true)
                .build()
        val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setConstraints(constraints)
                .build()
        val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java).build()
        WorkManager.getInstance(this).
        beginWith(oneTimeWorkRequest)
                .then(blurRequest)
                .enqueue();

Canceling Work

We can cancel ongoing works with methods in WorkManager instance.

  • cancelAllWork()
  • cancelAllWorkByTag
  • cancelUniqueWork
  • cancelWorkById

Work having unique id is replaced with newly enqueued work.

In the case of works have a unique id, if we enqueue work has the same name with existing work and ExistingWorkPolicty is set REPLACE, the existing work is immediately terminated.

val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setConstraints(constraints)
                .build()
        WorkManager.getInstance(this).cancelWorkById(oneTimeWorkRequest.id)

WorkManager Android Example

In this example, we are going to download image and blur image using work manager.

First we need to add worker class for download image.

DownloadWorker.kt

class DownloadWorker(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {

    override fun doWork(): Result {
        val myImageFileUri: Uri
        val client = OkHttpClient()
        val request = Request.Builder()
                .url("https://i.pinimg.com/originals/49/70/17/497017869c892b73b128ff72f2732035.jpg")
                .build()
        try {
            val response = client.newCall(request).execute()
            val bitmap = BitmapFactory.decodeStream(response.body?.byteStream())
            myImageFileUri = writeBitmapToFile(context,bitmap)
        } catch (e: Exception) {
            e.printStackTrace()
            return Result.failure()
        }
        val outputData = workDataOf(KEY_IMAGE_URI to myImageFileUri.toString())

        return Result.success(outputData)
    }
}

Then, we need to blur image using BlurWorker class.

BlurWorker.kt

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override fun doWork(): Result {
        val appContext = applicationContext

        val resourceUri = inputData.getString(KEY_IMAGE_URI)

        makeStatusNotification("Blurring image", appContext)
        sleep()

        return try {
            if (TextUtils.isEmpty(resourceUri)) {
                //Timber.e("Invalid input uri")
                throw IllegalArgumentException("Invalid input uri")
            }

            val resolver = appContext.contentResolver

            val picture = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)))

            val output = blurBitmap(picture, appContext)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(appContext, output)

            val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

            Result.success(outputData)
        } catch (throwable: Throwable) {
            //Timber.e(throwable, "Error applying blur")
            Result.failure()
        }
    }
}

WorkerUtils.kt

fun makeStatusNotification(message: String, context: Context) {

    // Make a channel if necessary
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        val name = VERBOSE_NOTIFICATION_CHANNEL_NAME
        val description = VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION
        val importance = NotificationManager.IMPORTANCE_HIGH
        val channel = NotificationChannel(CHANNEL_ID, name, importance)
        channel.description = description

        // Add the channel
        val notificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?

        notificationManager?.createNotificationChannel(channel)
    }

    // Create the notification
    val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle(NOTIFICATION_TITLE)
            .setContentText(message)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setVibrate(LongArray(0))

    // Show the notification
    NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build())
}

/**
 * Method for sleeping for a fixed about of time to emulate slower work
 */
fun sleep() {
    try {
        Thread.sleep(DELAY_TIME_MILLIS, 0)
    } catch (e: InterruptedException) {
        //Timber.e(e.message)
    }

}

/**
 * Blurs the given Bitmap image
 * @param bitmap Image to blur
 * @param applicationContext Application context
 * @return Blurred bitmap image
 */
@WorkerThread
fun blurBitmap(bitmap: Bitmap, applicationContext: Context): Bitmap {
    lateinit var rsContext: RenderScript
    try {

        // Create the output bitmap
        val output = Bitmap.createBitmap(
                bitmap.width, bitmap.height, bitmap.config)

        // Blur the image
        rsContext = RenderScript.create(applicationContext, RenderScript.ContextType.DEBUG)
        val inAlloc = Allocation.createFromBitmap(rsContext, bitmap)
        val outAlloc = Allocation.createTyped(rsContext, inAlloc.type)
        val theIntrinsic = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
        theIntrinsic.apply {
            setRadius(10f)
            theIntrinsic.setInput(inAlloc)
            theIntrinsic.forEach(outAlloc)
        }
        outAlloc.copyTo(output)

        return output
    } finally {
        rsContext.finish()
    }
}

/**
 * Writes bitmap to a temporary file and returns the Uri for the file
 * @param applicationContext Application context
 * @param bitmap Bitmap to write to temp file
 * @return Uri for temp file with bitmap
 * @throws FileNotFoundException Throws if bitmap file cannot be found
 */
@Throws(FileNotFoundException::class)
fun writeBitmapToFile(applicationContext: Context, bitmap: Bitmap): Uri {
    val name = String.format("blur-filter-output-%s.png", UUID.randomUUID().toString())
    val outputDir = File(applicationContext.externalCacheDir, OUTPUT_PATH)
    if (!outputDir.exists()) {
        outputDir.mkdirs() // should succeed
    }
    val outputFile = File(outputDir, name)
    var out: FileOutputStream? = null
    try {
        out = FileOutputStream(outputFile)
        bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, out)
    } finally {
        out?.let {
            try {
                it.close()
            } catch (ignore: IOException) {
            }

        }
    }
    return Uri.fromFile(outputFile)
}

Finally, in your MainAcitivity.kt call the worker class.

MainAcitivity.kt

class MainActivity : AppCompatActivity() {

    private var downloadedImageUri: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val imageViewNormal = findViewById<ImageView>(R.id.imageViewNormal)
        val imageViewBlur = findViewById<ImageView>(R.id.imageViewBlur)
        val blurImage = findViewById<Button>(R.id.blurImage)
        val downloadImage = findViewById<Button>(R.id.downloadImage)

        val oneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java).build()
        val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)

        WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneTimeWorkRequest.id).observe(this, { workInfo ->
            val imageUri = workInfo?.outputData?.getString(KEY_IMAGE_URI)
           imageUri?.let {
               imageViewNormal.setImageURI(Uri.parse(imageUri))
               downloadedImageUri = it
           }
        })

        downloadImage.setOnClickListener {
            WorkManager.getInstance(this).enqueue(oneTimeWorkRequest)

            WorkManager.getInstance(this).
            beginWith(oneTimeWorkRequest)
                    .then(blurRequest.build())
                    .enqueue()
        }

        blurImage.setOnClickListener {
            val builder = Data.Builder()
            builder.putString(KEY_IMAGE_URI, downloadedImageUri)
            blurRequest.setInputData(builder.build())
            val blurBuilder = blurRequest.build()
            WorkManager.getInstance(this).getWorkInfoByIdLiveData(blurBuilder.id).observe(this@MainActivity, { workInfo2 ->
                val imageUri2 = workInfo2?.outputData?.getString(KEY_IMAGE_URI)
                imageUri2?.let {
                    imageViewBlur.setImageURI(Uri.parse(imageUri2))
                }
            })
            WorkManager.getInstance(this@MainActivity).enqueue(blurBuilder)
        }

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageViewNormal"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
    <ImageView
        android:id="@+id/imageViewBlur"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintTop_toBottomOf="@id/imageViewNormal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
    <Button
        android:id="@+id/downloadImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Image Download"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/imageViewBlur" />
    <Button
        android:id="@+id/blurImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Blur Image"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/downloadImage" />
</androidx.constraintlayout.widget.ConstraintLayout>

Constants.kt

@JvmField val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence =
        "Verbose WorkManager Notifications"
const val VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION =
        "Shows notifications whenever work starts"
@JvmField val NOTIFICATION_TITLE: CharSequence = "WorkRequest Starting"
const val CHANNEL_ID = "VERBOSE_NOTIFICATION"
const val NOTIFICATION_ID = 1

// The name of the image manipulation work
const val IMAGE_MANIPULATION_WORK_NAME = "image_manipulation_work"

// Other keys
const val OUTPUT_PATH = "blur_filter_outputs"
const val KEY_IMAGE_URI = "KEY_IMAGE_URI"
const val TAG_OUTPUT = "OUTPUT"

const val DELAY_TIME_MILLIS: Long = 3000

Thanks for reading. you can download this example in GITHUB.

Leave a Reply

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