Android Kotlin: WorkManager input output data example

Introduction

WorkManager is a library in Android Jetpack that allows developers to schedule deferrable, asynchronous tasks that are guaranteed to execute even if the app exits or the device is restarted. This makes it highly suitable for background tasks that require guaranteed execution, such as downloading images, syncing data, or sending logs. In this article, we’ll explore an example of using WorkManager in Kotlin to download an image from the internet, manage input and output data, and display the result on the screen. This example showcases how WorkManager simplifies task scheduling and ensures task completion even in challenging circumstances.

In addition to providing an overview of the WorkManager implementation, this breakdown will delve into key components such as input data, output data, observing task states, and handling background processes efficiently. By the end of this walkthrough, you'll have a good understanding of how to implement WorkManager for tasks requiring data transfer and persistent task execution in Android.

MainActivity Overview

The heart of this example lies in MainActivity.kt, where the interaction with the user is handled. The layout features a button (btnDownload) that initiates the process, a TextView to show the status of the download, and an ImageView to display the downloaded image.

When the user clicks the "Download Image" button, it triggers a WorkManager task. The first step is defining the input data needed for the task. In this case, the input is a URL string (imageUrl) for the image to be downloaded. This input data is bundled into a Data.Builder object and passed to the WorkManager task. The task itself is represented by a OneTimeWorkRequest, which is designed to run only once.

Once the WorkRequest is created, it is enqueued using WorkManager.getInstance().enqueue(). This schedules the task to be executed by WorkManager. At the same time, LiveData is used to observe the status of the task through getWorkInfoByIdLiveData(). The state of the task, whether enqueued, running, or finished, is displayed in the TextView. Additionally, a toast notification is triggered to reflect the current state.

WorkManager and Worker Class

The image download process is handled in a background thread by the DownloadImageWorker class, which extends Worker. The doWork() method contains the core logic for downloading the image and saving it to internal storage.

The doWork() method begins by retrieving the imageUrl passed via the input data. This URL is then converted into a HttpURLConnection object, which establishes a connection to the server. Once the connection is made, the input stream of data is retrieved and converted into a Bitmap using BitmapFactory.decodeStream(). This Bitmap object represents the downloaded image.

If the image is successfully downloaded, it is saved to the internal storage of the app using the custom saveToInternalStorage() extension function. The saved file's URI is then returned as part of the output data, indicating a successful task completion. If the download fails for any reason, an error is logged, and a failure result is returned.

Input and Output Data

Input and output data are crucial parts of WorkManager tasks. In this example, input data is used to pass the URL of the image to the worker. The Data.Builder class allows you to pass key-value pairs to the worker, making it easy to handle dynamic data.

The output data is generated in the doWork() method after the image is successfully downloaded and saved. The createOutputData() method creates a Data object containing the URI of the saved image. This output data is then accessed in the MainActivity using LiveData. Once the worker has finished and if the task is successful, the ImageView displays the downloaded image using the URI provided in the output data.

Handling Task States

The application uses LiveData to monitor the state of the WorkManager task. WorkManager provides several states, including ENQUEUED, RUNNING, SUCCEEDED, FAILED, and CANCELLED. These states help in providing feedback to the user during various stages of task execution.

For example, when the task is enqueued, the TextView updates to show "Download enqueued." If the task is blocked or in progress, corresponding messages are displayed. Finally, when the task completes, either successfully or with failure, the UI is updated accordingly. If the task succeeds, the image is shown in the ImageView, and if it fails, an error message is displayed.

Bitmap Saving and Utilities

The custom extension function saveToInternalStorage() in xBitmap.kt handles the task of saving the downloaded bitmap to the device’s internal storage. It creates a unique file name using UUID.randomUUID(), compresses the bitmap, and writes it to an output stream. The function returns the URI of the saved file, which is then passed back to the main activity via output data.

Additionally, the helper function stringToURL() is used to convert the URL string into a proper URL object. This is a simple utility to handle the potential MalformedURLException that could arise during string-to-URL conversion.

Summary

This Android Kotlin example demonstrates how WorkManager can be used to manage background tasks such as downloading images, handling input and output data, and updating the UI based on task states. The separation of concerns is clear, with the MainActivity managing UI updates and user interactions, while the DownloadImageWorker handles the background task of image downloading and saving.

By using WorkManager, the application ensures that the task will complete, even if the app is closed or the device is restarted, making it a robust solution for deferred tasks in Android development. This example provides a foundation for building more complex background tasks using WorkManager in Android.


MainActivity.kt

package com.cfsuman.jetpack

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import android.graphics.BitmapFactory
import java.io.BufferedInputStream
import java.io.IOException
import java.net.HttpURLConnection
import android.graphics.Bitmap
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.work.*


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Download image
        btnDownload.setOnClickListener {
            // Define the input data for work manager
            val data = Data.Builder()
            data.putString("imageUrl",
                "https://www.freeimageslive.com/galleries/food/breakfast/pics/muffin1964.jpg")

            // Create an one time work request
            val downloadImageWork = OneTimeWorkRequest
                .Builder(DownloadImageWorker::class.java)
                .setInputData(data.build())
                .build()

            // Enqueue the work
            WorkManager.getInstance().enqueue((downloadImageWork))

            // Get the work status using live data
            WorkManager.getInstance().getWorkInfoByIdLiveData(downloadImageWork.id)
                .observe(this, Observer { workInfo ->

                    // Toast the work state
                    toast(workInfo.state.name)

                    if (workInfo != null) {
                        if (workInfo.state == WorkInfo.State.ENQUEUED) {
                            // Show the work state in text view
                            textView.text = "Download enqueued."
                        } else if (workInfo.state == WorkInfo.State.BLOCKED) {
                            textView.text = "Download blocked."
                        } else if (workInfo.state == WorkInfo.State.RUNNING) {
                            textView.text = "Download running."
                        }
                    }

                    // When work finished
                    if (workInfo != null && workInfo.state.isFinished) {
                        if (workInfo.state == WorkInfo.State.SUCCEEDED) {
                            textView.text = "Download successful."

                            // Get the output data
                            val successOutputData = workInfo.outputData
                            val uriText = successOutputData.getString("uriString")

                            // If uri is not null then show it
                            uriText?.apply {
                                // If download finished successfully then show the downloaded image in image view
                                imageView.setImageURI(Uri.parse(uriText))
                                textView.text = uriText
                            }
                        } else if (workInfo.state == WorkInfo.State.FAILED) {
                            textView.text = "Failed to download."
                        } else if (workInfo.state == WorkInfo.State.CANCELLED) {
                            textView.text = "Work request cancelled."
                        }
                    }
                })
        }
    }
}


// Worker class to download image
class DownloadImageWorker(context:Context, params: WorkerParameters): Worker(context,params){
    override fun doWork(): Result {
        // Get the input data
        val urlString = inputData.getString("imageUrl")
        val url = stringToURL(urlString)

        // IMPORTANT - Put internet permission on manifest file
        var connection: HttpURLConnection? = null

        try {
            // Initialize a new http url connection
            connection = url?.openConnection() as HttpURLConnection

            // Connect the http url connection
            connection.connect()

            // Get the input stream from http url connection
            val inputStream = connection.getInputStream()

            // Initialize a new BufferedInputStream from InputStream
            val bufferedInputStream = BufferedInputStream(inputStream)

            // Convert BufferedInputStream to Bitmap object

            // Return the downloaded bitmap
            val bmp:Bitmap? = BitmapFactory.decodeStream(bufferedInputStream)
            val uri:Uri? = bmp?.saveToInternalStorage(applicationContext)

            Log.d("download","success")
            // Return the success with output data
            return Result.success(createOutputData(uri))

        } catch (e: IOException) {
            e.printStackTrace()
            Log.d("download",e.toString())

        } finally {
            // Disconnect the http url connection
            connection?.disconnect()
        }
        Log.d("download","failed")
        return Result.failure(createOutputData(null))
    }


    // Method to create output data
    private fun createOutputData(uri: Uri?): Data {
        return Data.Builder()
            .putString("uriString", uri.toString())
            .build()
    }
}
xBitmap.kt

package com.cfsuman.jetpack

import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.net.Uri
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.net.MalformedURLException
import java.net.URL
import java.util.*


// Extension function to save bitmap in internal storage
fun Bitmap.saveToInternalStorage(context: Context): Uri {
    // Get the context wrapper instance
    val wrapper = ContextWrapper(context)

    // Initializing a new file
    // The bellow line return a directory in internal storage
    var file = wrapper.getDir("images", Context.MODE_PRIVATE)


    // Create a file to save the image
    file = File(file, "${UUID.randomUUID()}.png")

    try {
        // Get the file output stream
        val stream: OutputStream = FileOutputStream(file)

        // Compress bitmap
        this.compress(Bitmap.CompressFormat.PNG, 100, stream)

        // Flush the stream
        stream.flush()

        // Close stream
        stream.close()
    } catch (e: IOException){ // Catch the exception
        e.printStackTrace()
    }

    // Return the saved image uri
    return Uri.parse(file.absolutePath)
}


// Custom method to convert string to url
fun stringToURL(urlString: String?): URL? {
    urlString?.let {
        try {
            return URL(urlString)
        } catch (e: MalformedURLException) {
            e.printStackTrace()
        }
    }

    return null
}


fun Context.toast(message:String)=
    Toast.makeText(this,message, Toast.LENGTH_SHORT).show()
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rootLayout"
        tools:context=".MainActivity"
        android:background="#fdfdfc">
    <Button
            android:text="Download Image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnDownload"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
    <ImageView
            android:layout_width="0dp"
            android:layout_height="0dp"
            tools:srcCompat="@tools:sample/avatars"
            android:id="@+id/imageView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintHorizontal_bias="0.0"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"/>
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@+id/btnDownload"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            android:textColor="#3F51B5"
            tools:text="Work Status"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

// WorkManager
def work_version = "1.0.0-beta03"
implementation "android.arch.work:work-runtime:$work_version"