Implementing Work Manager in Android

In this 2nd part of the Work Manager series, we’ll be taking a look at how to implement work manager in android.

In the previous post, I talked about what Work Manager is and why you should start considering Work Manager for scheduling tasks in your Android applications. Be sure to check that out.

Implementing Work Manager in Android is really easy and I’ve divided the entire process into the following steps:

  • Setting up the project
  • Creating a OneTime/Periodic Request
  • Creating a Worker

We’ll be creating a simple app for downloading 3 images and displaying them in 3 corresponding ImageViews.

You can find the entire code for this application on GitHub: https://github.com/Ayusch/WorkManagerDemo

 

Setting up the project

Create a new project in Android Studio with a Blank Activity. Add the following dependencies in the app level build.gradle file:

implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
// Kotlin + coroutines
implementation "android.arch.work:work-runtime-ktx:1.0.0"
// optional - RxJava2 support
implementation "android.arch.work:work-rxjava2:1.0.0"

implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.squareup.okhttp3:okhttp:3.13.1'
implementation 'org.greenrobot:eventbus:3.1.1'

Now it’s time to create a basic Android application with a ScrollView and 3 ImageViews.

Add a ScrollView and 3 ImageViews in your app. Your layout file should look like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_1"
                android:layout_width="300dp"
                android:layout_height="300dp"
                android:layout_marginTop="32dp"
                android:scaleType="fitXY" />

            <ImageView
                android:id="@+id/iv_2"
                android:layout_width="300dp"
                android:layout_height="300dp"
                android:layout_marginTop="32dp"
                android:scaleType="fitXY" />

            <ImageView
                android:id="@+id/iv_3"
                android:layout_width="300dp"
                android:layout_height="300dp"
                android:layout_marginTop="32dp"
                android:scaleType="fitXY" />

        </LinearLayout>
    </ScrollView>


    <Button
        android:id="@+id/btn_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:text="Start Download" />

</LinearLayout>

There is also a button to start downloading the images. This is all you need in your layout file.

 

Creating a OneTimeRequest

Our Worker needs 2 things:

  • List of images to download.
  • Constraints under which the work should begin (eg: network connected, battery not low, etc…)

For a list of images, I’m using https://jsonplaceholder.typicode.com/photos which will give me a fake API to send a request to. We’ll be using the first 3 images. The JSON string will be placed in our MainActivity.java class.

To pass this JSON string to our worker we’ll create a Data object and put our string inside it with a key. It is just like a map where we can get our data using a specific key. Here’s how we’ll create out data object:

val data = Data.Builder()
    .putString("images", jsonString)
    .build()

Now it’s time to specify some constraints:

In our case, the only constraint is that the device should be connected to the network. This is how we create a constraint object for Work Manager:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)

Now it’s time to bind everything together into a single OneTimeRequest. OneTimeRequest also follows the builder pattern just as Data and Constraint.

We pass in our worker to the request (we’ll be creating our worker in the next step) and specify our data and constraints. We also add a tag to our request. We’ll talk about the tags in work manager later on. Finally, we call the build() method to build our request.

val oneTimeRequest = OneTimeWorkRequest.Builder(ImageDownloadWorker::class.java)
    .setInputData(data)
    .setConstraints(constraints.build())
    .addTag("demo")
    .build()

Now in order to enqueue the work, we’ll get an instance of the work manager and call. enqueueUniqueWork(), we pass a tag to the work, a work policy, and our OneTimeRequest.

WorkManager.getInstance().enqueueUniqueWork("OYO",ExistingWorkPolicy.KEEP,oneTimeRequest)

It’s time to create our worker which will do the actual task of downloading and storing the images.

 

Creating the Worker

Create a class named ImageDownloadWorker and extend it from Worker class from Work Manager. It takes two parameters, a context and work parameters which are being implicitly passed to it during the creation of worker.

Now, the only thing you need to do is override the doWork() method of this class, this method operates on a background thread and this is the place to do all your background work.

We’ll first get our input json string and decode it into a list of Image objects using Gson. Image is a simple data class which takes 5 parameters including the link to the image. Then, for each image, we’ll download and store the image in internal storage and finally display it in the associated ImageView.

class ImageDownloadWorker(private val mContext: Context, workerParameters: WorkerParameters) :
    Worker(mContext, workerParameters) {

    @SuppressLint("RestrictedApi", "CheckResult")
    override fun doWork(): Result {
        Log.d("ayusch", Thread.currentThread().toString())
        displayNotification(ProgressUpdateEvent("Please wait...", 3, 0))
        val imagesJson = inputData.getString("images")
        val gson = Gson()
        val listOfImages = gson.fromJson<List<Image>>(imagesJson, object : TypeToken<List<Image>>() {}.type);

        listOfImages.forEachIndexed { index, image ->
            downloadImage(image, index)
        }

        notificationManager.cancel(notificationId)
        return Result.Success()
    }

}

data class Image(var albumId: String, var id: String, var title: String, var url: String, var thumbnail: String)

Here is the downloadImage() function:

@SuppressLint("CheckResult")
private fun downloadImage(image: Image, index: Int) {
    val client = OkHttpClient()
    val request = Request.Builder()
        .url(image.url)
        .build()

    try {
        val response = client.newCall(request).execute()
        val bitmap = BitmapFactory.decodeStream(response.body()?.byteStream())

        ImageUtil.saveBitmap(mContext, bitmap, image.title).subscribe({ img ->
            displayNotification(ProgressUpdateEvent(image.title, 3, index + 1))
            EventBus.getDefault().post(ImageDownloadedEvent(img, image.title, image.id))
        }, { error ->
            error.printStackTrace()
        })

    } catch (e: Exception) {
        e.printStackTrace()
    }
}

We use OkHttp as our client to download images. Notice, that I’ve used execute() for downloading images and not enqueue() since we’re already on a background thread and enqueue() spawns another thread which can lead to unexpected behavior.

Then we decode the image using BitmapFactory and save it to our internal storage.

To save the bitmap, create a new kotlin object file, and create a saveBitmap() function. It’ll take a context, bitmap, and a file name and return a Single which we can subscribe to in our worker.

If you aren’t familiar with RxJava, I highly recommend having a look at these posts:

Our final save image function looks like this:

fun saveBitmap(context: Context, bitmap: Bitmap, filename: String): Single<String> {
    return Single.create<String> { emitter ->
        val stream = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 30, stream)
        val mediaByteArray = stream.toByteArray()

        try {
            val myDir = context.filesDir

            val path = "$myDir/media/"
            val secondFile = File("$myDir/media/", filename)

            if (!secondFile.parentFile.exists()) {
                secondFile.parentFile.mkdirs()
            }
            secondFile.createNewFile()

            val fos = FileOutputStream(secondFile)
            fos.write(mediaByteArray)
            fos.flush()
            fos.close()
            emitter.onSuccess(path)
        } catch (exception: IOException) {
            exception.printStackTrace()
            emitter.onError(exception)
        }
    }
}

Now that we’ve saved our bitmap, it’s time to display it in our activity. We’ll be using EventBus to send the saved image’s path to our activity which will then load the image using Picasso in the appropriate ImageView. To send the path, we’ll be using ImageDownloadedEvent which is another data class that contains the path, name, and id of the image.

Note: This is not the most optimal way of communicating with the worker but for the simplicity of this tutorial, we’ll be using EventBus

 

Displaying the Images

To finally display the images, let’s head over to MainActivity.kt and catch the event in onEvent method. We retrieve the file using the path and the name of the image obtained from ImageDownloadedEvent.

@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(imageDownloadedEvent: ImageDownloadedEvent) {
    val file = File("${imageDownloadedEvent.path}/${imageDownloadedEvent.name}")
    when (imageDownloadedEvent.id) {
        "1" -> Picasso.get().load(file).into(iv_1)
        "2" -> Picasso.get().load(file).into(iv_2)
        "3" -> Picasso.get().load(file).into(iv_3)
    }
}

Finally, using when() we load the image into appropriate imageview. Our application is done and dusted !!

 

*Word of Caution*

If you’ve read the previous post of this Work Manager series, you’ll know that Work Manager should only be used for deferrable tasks (tasks that need not be run immediately) but the application we just built is an example of a task that would most of the time need to be run immediately ie: Non-deferrable.

For these kinds of applications, you should be using the download manager API to download the image as it starts immediately as it is called while the work manager starts with some latency.

 

Conclusion                                                            

This article outlines how to create a one-time request to perform background tasks with the Work Manager in your android application. In the coming posts, I’ll outline some nuances of Work Manager which you should definitely know about before considering using work manager.

 

*Important*: I’ve created a SLACK  workspace for mobile developers where we can share our learnings about everything latest in Tech, especially in Android Development, RxJava, Kotlin, Flutter, and overall mobile development in general.

Click on this link to join the slack workspace. It’s absolutely free!

 

Like what you read? Don’t forget to share this post on FacebookWhatsapp, and LinkedIn.

You can follow me on LinkedInQuoraTwitter, and Instagram where I answer questions related to Mobile Development, especially Android and Flutter.

If you want to stay updated with all the latest articles, subscribe to the weekly newsletter by entering your email address in the form on the top right section of this page.