Android Background Processing

Sep 23 2022 · Kotlin 1.6, Android 12, Android Studio Chipmunk 2021.2.1

Part 1: Run Background Work

03. Implement a Simple Worker

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. Launch Threads & Post to the Main Thread Next episode: 04. Expect a Result From Workers

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 03. Implement a Simple Worker

The student materials have been reviewed and are updated as of SEPTEMBER 2022. Image url has changed.

Transcript: 03. Implement a Simple Worker

After the brief revision of threading, you’re ready to start learning about more concepts in Android backround processing. The first concept you’ll cover is the WorkManager API.

[Short pause]

Android’s been around for quite a few years. This means it’s had plenty of time to change. Especially when it comes to background processing APIs.

One of the first APIs Android offered to schedule work was the AlarmManager API. As the name states, it’s used to manage work in a way that can wake up the device as an alarm, giving you the ability to react to the wakeup. It’s still used, but there are better options, which you will learn about.

After the AlarmManager came the AsyncTask API. An AsyncTask is an object which has built in base functionality to work in the background, process some result, and then post the result to the main thread.

It’s more wholesome than using the AlarmManager. However, it proved to be prone to memory leaks, and hard to understand from a beginner’s perspective.

Next came the JobScheduler API, in Android Lolipop. The API is like a combination of the AlarmManager API, and the AsyncTask API, or even Android Services, which you’ll cover in the third part of the course. The idea is to be able to schedule a Job, at a specific time, that will work in the background.

It proved to be useful, but communicating the result back was a problem, as you had to use BroadcastReceivers to send small messages between Android components.

But because JobScheduler came late in the Android timeline, and because the previous concepts all had a different syntax, everyone wanted a single way to write their background processing code.

And so, the WorkManager API came to life. An API which you use to schedule work, with constraints such as internet connection, battery level, or storage. And from each Worker you create, you can return a result. You can also chain workers and their results! Really useful!

WorkManager is backwards compatible with all Android APIs. This means you can create a Worker, and it will respect the API levels, and the APIs it can use, to finish the job.

But enough theory talk! Let’s implement a simple Worker, to download an image! :]

To use the WorkManager API, you need a specific dependency. Open the in the app level build.gradle file, and add the following line of code:

implementation "androidx.work:work-runtime-ktx:2.3.4"

Also add the following Kotlin & compile options, to allow for advanced WorkManager usage:

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = JavaVersion.VERSION_1_8
  }

Sync the project, to use the API. The work-runtime-ktx dependency adds necessary classes and extra behavior for the WorkManager. And the compile options allow for advanced features of WorkManager, when it comes to syntax.

Now create a new class called DownloadWorker, and add the following code:

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

  override fun doWork(): Result {
    
  }
}

Every Worker has to extend the Worker class from the WorkManagerAPI. It has to receive a Context and WorkerParameters in the constructor, and pass it to the parent constructor.

Every worker has to override the doWork(), which describes what kind of work it will run. It returns a Result, which can be a success or a failure, similar to what you have implemented yourself in previous courses. To implement the download functionality, add the next snippet of code:

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

  override fun doWork(): Result {
    val imageUrl = URL("https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg")
    val connection = imageUrl.openConnection() as HttpURLConnection
    connection.doInput = true
    connection.connect()

    val imagePath = "owl_image.jpg"
    val inputStream = connection.inputStream
    val file = File(applicationContext.externalMediaDirs.first(), imagePath)
  }
}

You open an HTTP connection to the URL above, and then create a file to store the image in. You will store the image in the external directory for this app, so it’s easier to find, and you don’t have to use any special content providers. Now add the following:

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

  override fun doWork(): Result {
    val imageUrl = URL("https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg")
    val connection = imageUrl.openConnection() as HttpURLConnection
    connection.doInput = true
    connection.connect()

    val imagePath = "owl_image.jpg"
    val inputStream = connection.inputStream
    val file = File(applicationContext.externalMediaDirs.first(), imagePath)

    val outputStream = FileOutputStream(file)
    outputStream.use { output ->
      val buffer = ByteArray(4 * 1024)

      var byteCount = inputStream.read(buffer)
      while (byteCount > 0) {
        output.write(buffer, 0, byteCount)

        byteCount = inputStream.read(buffer)
      }

      output.flush()
    }

    return Result.success()
  }
}

After reading the bytes using an InputStream, one chunk at a time, you’re writing them to a file, and finally returning a successful Result. You have to use the FileOutputStream to write the file, and the buffer to slowly read the file, in a buffered way. By returning a successful result, you tell the WorkManager that this worker finished its work on the happy path!

That’s all for the Worker, now proceed to the MainActivity.kt, and add the code to build constraints for the WorkManager:

private fun downloadImage() {
  val constraints = Constraints.Builder()
      .setRequiresBatteryNotLow(true)
      .setRequiresStorageNotLow(true)
      .setRequiredNetworkType(NetworkType.NOT_ROAMING)
      .build()
}

You first create a function to download the image, to separate the code. Then you build necessary constraints for the Worker. Constraints in WorkManager are used to schedule the Work only in certain conditions. Using these constraints, you will run the worker only if the battery and storage are not low, and if the user is not using roaming.

Now build the work request:

private fun downloadImage() {
  val constraints = Constraints.Builder()
      .setRequiresBatteryNotLow(true)
      .setRequiresStorageNotLow(true)
      .setRequiredNetworkType(NetworkType.NOT_ROAMING)
      .build()

  val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
      .setConstraints(constraints)
      .build()
}

There are two ways of building workers. As a one-time request, or as a repeating process. In this case, as you’re downloading a file, you’ll use a one-time worker.

With the OneTimeWorkRequestBuilder, you can specify which worker class you will build, and then you add necessary constraints to it. Now queue the worker, using the WorkManager:

private fun downloadImage() {
  val constraints = Constraints.Builder()
      .setRequiresBatteryNotLow(true)
      .setRequiresStorageNotLow(true)
      .setRequiredNetworkType(NetworkType.NOT_ROAMING)
      .build()

  val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
      .setConstraints(constraints)
      .build()

  val workManager = WorkManager.getInstance(this)

  workManager.enqueue(downloadRequest)
}

You fetch the WM instance using a Context object, and then you enqueue the work you want to finish. But how will you know the worker has finished? To learn this, you need to observe the work’s info and status. Do that the following way:

private fun downloadImage() {
  val constraints = Constraints.Builder()
      .setRequiresBatteryNotLow(true)
      .setRequiresStorageNotLow(true)
      .setRequiredNetworkType(NetworkType.NOT_ROAMING)
      .build()

  val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
      .setConstraints(constraints)
      .build()

  val workManager = WorkManager.getInstance(this)

  workManager.enqueue(downloadRequest)

  workManager.getWorkInfoByIdLiveData(downloadRequest.id).observe(this, Observer { info ->
    if (info.state.isFinished) {
      val imageFile = File(externalMediaDirs.first(), "owl_image.jpg")
      displayImage(imageFile.absolutePath)
    }
  })
}

You fetch the LiveData that holds the WorkInfo by the worker id, and then attach an observer to it. Once the state of the worker isFinished, you can load the image file, and display it. But because the display image function doesn’t exist, implement it like so:

private fun displayImage(imagePath: String) {
  GlobalScope.launch(Dispatchers.Main) {
    val bitmap = loadImageFromFile(imagePath)

    image.setImageBitmap(bitmap)
  }
}

private suspend fun loadImageFromFile(imagePath: String): Bitmap =
    withContext(Dispatchers.IO) { BitmapFactory.decodeFile(imagePath) }

With the help of Kotlin Coroutines, you’re launching a coroutine bound to the main thread, and then processing the image Bitmap, in the background. Finally, you display the image, in the ImageView. Now call the downloadImage function instead of the old code you used to display it.

Run the project, and you should see the image appear in the app again, but this time, you’re using a WorkManager! :]