Skip to main content

Android Kotlin: WorkManager get status live data example

Introduction

In this article, we explore an example of how to use WorkManager in Android with Kotlin to manage background tasks and observe their status through LiveData. WorkManager provides a flexible and reliable solution for deferrable tasks that need guaranteed execution. By observing LiveData, the app can dynamically react to changes in the task's state. The example demonstrates downloading an image in the background and updating the UI based on the task's progress and outcome.

The example code consists of two main components: a WorkManager implementation that performs the image download, and a MainActivity that triggers the download and monitors its status. By following this example, developers will learn how to set up WorkManager, handle task status updates in real time, and display results on the UI.

MainActivity Overview

The MainActivity.kt file initializes the app's interface and manages interactions with the WorkManager. The layout contains a button (btnDownload) to start the download task, a TextView to display task status updates, and an ImageView to show the downloaded image.

The onCreate() method sets up an OnClickListener on the download button. When the user clicks the button, it triggers the creation of a one-time WorkRequest for the DownloadImageWorker, which performs the image download in the background. This work request is then enqueued using WorkManager.

To keep track of the task's progress, getWorkInfoByIdLiveData() is called on the work request’s ID. This returns LiveData that provides updates on the task's status. Using an Observer, the app listens for state changes, updating the UI accordingly. For example, it shows different messages depending on whether the task is ENQUEUED, RUNNING, or BLOCKED. When the task finishes, the app checks if the task was successful and, if so, displays the downloaded image in the ImageView.

WorkManager and DownloadImageWorker

The DownloadImageWorker class performs the actual background task of downloading an image. This class extends Worker and overrides the doWork() method, which is where the image download is implemented. The image URL is defined in the code, and the process begins by opening an HttpURLConnection to download the image.

The image is downloaded into a Bitmap object using a BufferedInputStream. After downloading, the image is saved to the device's internal storage using the extension function saveToInternalStorage(). If the task succeeds, Result.success() is returned; otherwise, Result.failure() is returned.

In case of an error, such as an IOException during the download, the task gracefully handles the exception by logging the error and returning a failure result. This ensures that even if something goes wrong during the background process, the app can respond appropriately.

Saving and Accessing Images

The downloaded image is saved to the app's internal storage using a custom extension function Bitmap.saveToInternalStorage(). This function generates a unique filename for each image using a UUID and compresses the bitmap into a PNG format before writing it to the storage. The method returns the file URI, which can later be used to retrieve and display the image in the ImageView.

Additionally, two other extension functions—countSavedBitmap and savedBitmapList—provide utility methods for counting and retrieving the list of saved images in internal storage. These functions use the ContextWrapper class to manage access to the internal storage directory, where images are saved.

XML Layout and Gradle Dependencies

The activity_main.xml layout file defines the user interface. It consists of a Button to initiate the download, a TextView to display status updates, and an ImageView to display the downloaded image. The layout uses ConstraintLayout for flexible positioning of the UI components.

In the build.gradle file, WorkManager is added as a dependency using version 1.0.0-beta03. This library provides the necessary APIs to manage background tasks and ensures compatibility with Android's power and memory management features.

Summary

This example showcases how to use WorkManager to perform background tasks in Android and observe their progress using LiveData. The app enqueues a work request to download an image in the background, providing real-time status updates to the user interface. By handling task states such as ENQUEUED, RUNNING, SUCCEEDED, and FAILED, the app creates a responsive and user-friendly experience.

By leveraging WorkManager's robust scheduling features and Kotlin's concise syntax, developers can easily implement reliable background tasks in their apps. This approach is particularly useful for tasks that require guaranteed execution, such as downloading files, syncing data, or performing long-running computations.


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 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{
            // Create an one time work request
            val downloadImageWork = OneTimeWorkRequest
                .Builder(DownloadImageWorker::class.java).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."
                            // If download finished successfully the show the downloaded image in image view
                            imageView.setImageURI(nameToUri(savedBitmapList[countSavedBitmap-1]))
                            //textView.text = savedBitmapList[savedBitmapList.count()-1]
                        }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 {
        // Do the work here
        val url = stringToURL(
            "https://www.freeimageslive.com/galleries/food/fruitveg/pics/strawberry_bowl.jpg")

        // 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)
            bmp?.saveToInternalStorage(applicationContext)

            Log.d("download","success")
            return Result.success()

        } 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()
    }
}
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)
}


// Extension function to count internal storage images
val Context.countSavedBitmap:Int
    get(){
        val wrapper = ContextWrapper(this)
        val file = wrapper.getDir("images", Context.MODE_PRIVATE)
        val length = file.list().size
        return length
    }


// Extension function to get internal storage images list
val Context.savedBitmapList:MutableList<String>
    get(){
        val wrapper = ContextWrapper(this)
        val file = wrapper.getDir("images", Context.MODE_PRIVATE)
        val list = file.list().asList().toMutableList()
        return list
    }


fun Context.nameToUri(name:String):Uri{
    val wrapper = ContextWrapper(this)

    // 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, name)

    val uri = Uri.fromFile(file)

    return uri
}


// 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_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintHorizontal_bias="0.498"/>
    <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="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnDownload"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Large"
            android:textColor="#3F51B5"
    />
</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

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

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...