Scheduling Tasks With Android WorkManager

In this WorkManager tutorial, you’ll learn how to schedule different kinds of tasks, test the tasks, as well as debug different tasks. By Harun Wangereka.

4.6 (11) · 1 Review

Download materials
Save for later
Share

WorkManager is a useful and important component of Android Jetpack. It allows the app to do things in the background even when the app is exited or the device is restarted.

WorkManager also has many advantages over its predecessors. For instance, it’s battery conscious, allows you to determine the conditions for your task to run such as having a Wi-Fi connection, flexible retrying, and integration with Coroutines and RxJava.

In this tutorial, you’ll build WorkManagerApp. The app downloads an image from a URL and saves the image to the device in the background. During the process, you’ll learn about:

  • WorkManager basics
  • Creating different workers and querying work progress
  • WorkManager initialization types
  • Testing your workers

Getting Started

Download the materials using the Download Materials button at the top or bottom of this tutorial. Open Android Studio 4.1.2 or later and import the starter project.

Build and run the project. You’ll see the following screen:

WorkManager Start Download

Looking Into WorkManager

WorkManager is a Jetpack Library that allows you to run deferrable jobs.

To understand how WorkManager operates under the hood, you need to know how it interacts with the Android operating system regarding:

  • Persisting requests
  • Running your requests

Persisting Requests

Adapted from Android Dev Summit ’19 WorkManager Presentation

WorkManager Persist Request

Once you enqueue your work request, WorkManager stores it in the database, which acts as a Single Source of Truth.

After this, WorkManager sends the work request to JobScheduler for API 23 and above. For API below 23, WorkManager performs a further check. If the device has Play Services installed, WorkManager sends the work request to GCM Network Manager. If it’s not installed, WorkManager uses Alarm Manager to run the work.

WorkManager does all this for you. :] Next, you’ll look at how WorkManager actually runs your work request.

Running Your Request

Adapted from Android Dev Summit ’19 WorkManager Presentation

WorkManager Run Request

JobScheduler, GCM Network Manager and Alarm Manager are aware of your work. They’ll start your application if need be, given that the device meets all the constraints. In turn, they’ll request WorkManager to run your work.

There’s also GreedyScheduler, which runs inside the app process. Hence, it doesn’t start your app if it’s in the background. GreedyScheduler isn’t affected by the OS.

Now that you understand how WorkManager operates, it’s time to define your first WorkRequest.

Defining Your Work

You define your work by extending from Worker and overriding doWork. doWork is an asynchronous method that executes in the background.

Note: To learn more about WorkManager basics, checkout the WorkManager Tutorial for Android: Getting Started tutorial.

WorkManager has support for long-running tasks. It keeps the process in these tasks alive when the work is running. Instead of a normal Worker, you’ll use a ListenableWorker if you’re in Java or a CoroutineWorker if you’re using Kotlin Coroutines.

First, navigate to ImageDownloadWorker.kt inside the workers package. You’ll see a class like the one below:

class ImageDownloadWorker(
  private val context: Context,
  private val workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
  override suspend fun doWork(): Result {
    TODO("Not yet implemented")
  }
}

The code above does the following:

  • The ImageDownloadWorker class extends CoroutineWorker.
  • ImageDownloadWorker requires instances of Context and WorkerParameters as parameters in the constructor. You pass them to CoroutineWorker, too.
  • It overrides doWork(), a suspend method. It can do long-running tasks off the main thread.

doWork() is pretty bare. You’ll add the code to do work in this method shortly.

Navigate to ImageUtils.kt and add the following extension function:

fun Context.getUriFromUrl(): Uri? {
  // 1
  val imageUrl =
    URL(
      "https://images.pexels.com/photos/169573/pexels-photo-169573.jpeg" +
          "?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
    )
  // 2
  val bitmap = imageUrl.toBitmap()
  
  // 3
  var savedUri: Uri? = null
  bitmap?.apply {
    savedUri = saveToInternalStorage(this@getUriFromUrl)
  }
  return savedUri
}

Here’s a breakdown of what the code above does:

  1. imageUrl converts a link to a qualified URL.
  2. Next, you convert imageUrl to a bitmap using toBitmap().
  3. Last, you save the image bitmap to internal storage. Moreover, you get the URI of the path to the image’s storage location.

Head to ImageDownloadWorker.kt. Replace TODO(“Not yet implemented”) with the following code:

// 1
delay(10000)
// 2
val savedUri = context.getUriFromUrl()
// 3
return Result.success(workDataOf("IMAGE_URI" to savedUri.toString()))

Here’s a code breakdown:

  1. The image downloads quickly. To simulate a near real-world situation, you add a delay of 10,000 milliseconds so the work can take time.
  2. You get the URI from the extension function you added in the previous step.
  3. Last, once you have the URI, you return a successful response to notify that your work has finished without failure. workDataOf() converts a list of pairs to a Data object. A Data object is a set of key/value pairs used as inputs/outputs for ListenableWorker‘s. IMAGE_URI is a key for identifying the result. You’re going to use it to get the value from this worker.

Now, you might see some warnings due to missing imports. At the top of the file, add:

import androidx.work.workDataOf
import com.raywenderlich.android.workmanager.utils.getUriFromUrl
import kotlinx.coroutines.delay

You have defined your work. Next, you’ll create a WorkRequest to run the work.

Creating Your WorkRequest

For your scheduled work to run, the WorkManager service has to schedule it. A WorkRequest contains information of how and when your work will run.

You can schedule your work to run either periodically or once.

For running work periodically, you’ll use a PeriodicWorkRequestBuilder. For one-time work, you’ll use OneTimeWorkRequestBuilder.

Creating a One-Time WorkRequest

To create a one-time request, you’ll begin by adding WorkManager initialization. Navigate to HomeActivity.kt. Add this at the top class definition:

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

Here, you create an instance of WorkManager as a top-class variable. Members of HomeActivity can use this WorkManager instance. Be sure to add the WorkManager import when prompted to do so.

Second, add the following method below onCreate:

private fun createOneTimeWorkRequest() {
  // 1
  val imageWorker = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
    .setConstraints(constraints)
    .addTag("imageWork")
    .build()
  // 2
  workManager.enqueueUniqueWork(
    "oneTimeImageDownload",
    ExistingWorkPolicy.KEEP,
    imageWorker
  )
}

In the code above, you:

  1. Create your WorkRequest. You also set constraints to the work. Additionally, you add a tag to make your work unique. You can use this tag to cancel the work and observe its progress, too.
  2. Finally, you submit your work to WorkManager by calling enqueueUniqueWork.

You might see some warnings due to missing imports. At the top of the file, add:

import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import com.raywenderlich.android.workmanager.workers.ImageDownloadWorker

Still, you’ll see the constraints is still highlighted in red. To resolve this, add the following code below your WorkManager initialization at the top of the class:

private val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresStorageNotLow(true)
  .setRequiresBatteryNotLow(true)
  .build()

The following constraints are set for work to run. The device must have:

  • An active network connection
  • Enough storage
  • Enough battery

Inside onCreate, add the following code below requestStoragePermissions():

activityHomeBinding.btnImageDownload.setOnClickListener {
  showLottieAnimation()
  activityHomeBinding.downloadLayout.visibility = View.GONE
  createOneTimeWorkRequest()
}

In the code above, you call createOneTimeWorkRequest() when you tap START IMAGE DOWNLOAD. You’re also showing the animation and hiding the layout as you wait for your work to complete.

Build and run the app. The UI remains the same.

WorkManager Start Download

Tap START IMAGE DOWNLOAD. Notice that the animation doesn’t stop. Don’t worry, you’ll fix this behavior in the next steps.

Background Image Download