Work Manager Poster

How to run task with work manager in android ?

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, and 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 and 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.


Before getting started, check out another post on the jetpack,

View Binding in Android Jetpack [Updated]

Dependency injection on Android with Hilt[Example]

Room Database Android Example [Beginners]

[MVVM With Retrofit and Recyclerview in Kotlin Example


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 is 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 FirebaseJobDispatcher, GcmNetworkManager, 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.

Schedule tasks with WorkManager

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 Information.

Creating Your WorkRequest

To create a work request, 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 downloads 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 was 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().

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().

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 the 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 the 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 to 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.

“`kotlin
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).
* The 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.

kotlin
val myWorkRequest = OneTimeWorkRequestBuilder()
.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 the image in the application. We can chain the work for sequential processes or execute some works parallelly.

kotlin
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 a unique id is replaced with newly enqueued work.

In the case of works having a unique id, if we enqueue work that has the same name as 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 images and blur images using the work manager.

First, we need to add a worker class for downloading images.

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 images 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.


Comments

Leave a Reply

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


Latest Posts