Android Background Processing

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

Part 2: Use WorkManager in Complex Apps

10. Chain Work

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: 09. Implement WorkManager in Complex Apps Next episode: 11. Challenge - WorkManager

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: 10. Chain Work

The student materials have been reviewed and are updated as of SEPTEMBER 2022.

Transcript: 10. Chain Work

After building a worker, observing its status, and returning a result, you’re ready to build more workers, and chain them together! :]

In this episode you’ll first build a worker to upload an image to the server. You also have to fill in the RemoteServiceApi, to provide that API call for Retrofit. After you’re able to upload images, you’ll build another worker to check if the image is already downloaded, just like you did in the first part of the course.

You’ll chain this worker with the download worker, to make sure you don’t download the same image multiple times. There’s a lot of work to do, so let’s get on it!

To begin, create a new response class named UploadResponse.kt, in the model/response package, and add the following code:

@Serializable
class UploadResponse(val message: String = "", val url: String = "")

This will represent the response from the server, where you will get a message, and the download path for that image.

Then open the RemoteApiService.kt file, and add the following code:

  @Multipart
  @POST("/files")
  suspend fun uploadImage(
      @Part imageFile: MultipartBody.Part
  ): UploadResponse

To upload a file, you need a POST multipart request. A multipart is a form of sending data, where you can have multiple pieces of data to send, each being a part of the request.

In this case, you’ll send an image file to the server, so there’s only one part. Now open the RemoteApi.kt file, to implement this request. Add the following function and code:

suspend fun uploadImage(file: File): UploadResponse {
  val part: MultipartBody.Part = MultipartBody.Part.createFormData("file", file.name, file.asRequestBody())

  return apiService.uploadImage(part)
}

You create a MultipartBody.Part, using the helper function asRequestBody(), and a file. Then you return an UploadResponse, as a result.

This is all you need from the API side of things. You can now build a worker. Create a new file in the worker package, named UploadImageWorker, and add the following base code:

class UploadImageWorker(context: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(context, workerParameters) {

  override suspend fun doWork(): Result {

  }
}

Notice how this is extending a CoroutineWorker, instead of a regular worker. This is because the RemoteApi function is a suspend function, and it requires a coroutine to work.

And the WorkManager API has a special type of Workers, which support coroutines - the CoroutineWorker. Because of the coroutines support, the doWork() function is now a suspend function too.

To finish the worker, add the code which will receive an image path, and upload the image to the server:

class UploadImageWorker(context: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(context, workerParameters) {

  private val remoteApi by lazy { App.remoteApi }

  override suspend fun doWork(): Result {
    val imagePath = inputData.getString("image_path") ?: return Result.failure()

    val result = remoteApi.uploadImage(File(imagePath))

    return if (result.message == "Success!") {
      Result.success()
    } else {
      Result.failure()
    }
  }
}

You’re using the RemoteApi to upload the file, and then returning a result depending on the server response!

Good job, that’s the first worker! Now create another file, named LocalImageCheckWorker, and add the following code:

private const val NO_IMAGE = "noImage"

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

  override fun doWork(): Result {
    val imagePath = inputData.getString("image_path") ?: NO_IMAGE
    val parts = imagePath.split("/")

    if (imagePath.isBlank() || imagePath == NO_IMAGE) {
      val outputData = workDataOf("is_downloaded" to false)
      return Result.success(outputData)
    }

    val rootFile = applicationContext.externalMediaDirs.first()

    val lastSegment = parts.last()

    return try {
      val isDownloaded = rootFile.list()?.any { filePath -> lastSegment in filePath }
      val outputData = workDataOf("is_downloaded" to (isDownloaded ?: false))

      Result.success(outputData)
    } catch (error: Throwable) {
      error.printStackTrace()
      Result.failure()
    }
  }
}

This should be familiar to you, from the first part of the course. You check and return if the file is within the local storage.

Then in the download worker, you’ll check if the file exists, so you don’t download it multiple times. Open the DownloadImageWorker file, and change the code to the following:

val isAlreadyDownloaded = inputData.getBoolean("is_downloaded", false)
val imageDownloadPath = inputData.getString("image_path") ?: return Result.failure()
val parts = imageDownloadPath.split("/")

if (isAlreadyDownloaded) {
  val imageFile = File(applicationContext.externalMediaDirs.first(), parts.last())
  return Result.success(workDataOf("image_path" to imageFile.absolutePath))
}

To make sure you don’t download the same file more than once, you’re adding these checks! Also change the way images are being saved and downloaded to the following:

val imageUrl = URL("$BASE_URL/files/$imageDownloadPath")

...

val file = File(applicationContext.externalMediaDirs.first(), parts.last())

This way you’ll only take the last part of the image path as its name, and you’ll download files directly from the server’s files directory! :]

You’re almost done with the changes, there are still two small things you need to add. Head over to the ImageHolder, and change the image loading code to the following:

val localImagePath = BASE_URL + "/files/${image.imagePath}"

Glide.with(containerView).load(localImagePath).into(containerView.image)

Because you’re no longer loading images from a website, but rather from the local server, the image path is different. Head over to the ImagesFragment, and add the new worker for file checking:

val localImageCheckWorker = OneTimeWorkRequestBuilder<LocalImageCheckWorker>()
    .setInputData(workDataOf("image_path" to imageUrl))
    .build()
        
val workManager = WorkManager.getInstance(requireActivity())
workManager.beginWith(localImageCheckWorker)
    .then(downloadImageWorker)
    .enqueue()

By adding this worker before the download, you know you won’t download the same image twice!

This has been going on for a while, but one final thing! :] Head over to the SettingsFragment, and add the following code to the onActivityResult() function, to start the image upload worker:

val context = activity as? Context ?: return
val selectedImage = data?.data ?: return
val fileUri = FileUtils
    .getImagePathFromInputStreamUri(selectedImage, context.contentResolver, context)

val worker = OneTimeWorkRequestBuilder<UploadImageWorker>()
    .setInputData(workDataOf("image_path" to fileUri))
    .build()

WorkManager.getInstance(requireActivity())
  .enqueue(worker)

You’re almost done! :] Make sure to remove the hardcoded image from the ImagesFragment, and run the project at long last. Then try to upload an image! :] Once you’re done with uploading, try to download the image, and refresh the ImagesFragment.