Skip to main content

Android Kotlin: WorkManager constraints example

Introduction

In this article, we explore an Android Kotlin example using WorkManager to handle background tasks with specific constraints. WorkManager is part of Jetpack libraries and provides a simple yet flexible API for scheduling deferrable, guaranteed tasks that will run even if the app exits or the device restarts. In our example, we use WorkManager to download an image from a URL with certain constraints, such as the device being on charge and having a network connection.

This breakdown will guide you through the key components of this example, highlighting the WorkManager’s constraints, handling background tasks with worker classes, and the integration of UI elements to display the download status and the downloaded image.

MainActivity Overview

The main activity (MainActivity.kt) is the entry point for the application and contains the logic for triggering the download process. When the user clicks the "Download Image" button, a WorkRequest is created with constraints. These constraints ensure that the download only starts when the device is connected to a network and charging. This is achieved using a Constraints.Builder object, which is then attached to the WorkRequest.

The constraints defined here include setRequiresCharging(true), which ensures that the device is charging before the task starts, and setRequiredNetworkType(NetworkType.CONNECTED), which ensures a network connection is available. Additional constraints, like requiring the battery to not be low, can be added as needed but are commented out in this example for simplicity.

Setting Up WorkManager

To start the download, we enqueue a OneTimeWorkRequest using the WorkManager class. This request is linked to the DownloadImageWorker class, which handles the actual downloading in the background. We also pass input data, including the URL of the image to download, using the Data.Builder().

Once the work is enqueued, we observe its status using getWorkInfoByIdLiveData(), which allows real-time updates on the task’s progress. This status is reflected in the UI through a TextView, showing different states such as "Download enqueued," "Download running," and finally either "Download successful" or "Failed to download," depending on the outcome.

Downloading the Image with Worker Class

The DownloadImageWorker class is responsible for performing the background task. It extends the Worker class, and the doWork() method is overridden to define the actual task logic. The image is downloaded from the provided URL using an HTTP connection, and the input stream is converted into a Bitmap object.

The downloaded image is then saved to the device's internal storage using a custom extension function, saveToInternalStorage(). This function saves the bitmap to a private directory and returns the URI of the saved image. If the download is successful, Result.success() is returned along with the URI, which is passed back to MainActivity to display the image. In case of failure, Result.failure() is returned.

Saving the Image to Internal Storage

The extension function saveToInternalStorage() handles saving the downloaded image in the internal storage of the device. It creates a directory inside the app's internal storage and saves the image as a PNG file. This function is useful because it abstracts away the complexities of file handling, returning the URI of the saved file, which can be used later to display the image in an ImageView.

Additionally, the helper function stringToURL() safely converts a string to a URL object, catching any potential MalformedURLException that may occur during the conversion.

XML Layout and UI Interaction

The layout file (activity_main.xml) defines the user interface, which includes a Button, a TextView, and an ImageView. When the user presses the "Download Image" button, the download process begins, and the TextView displays the status of the download. Upon successful completion, the downloaded image is displayed in the ImageView using the URI retrieved from the DownloadImageWorker.

The layout is simple and designed to focus on the core functionality: downloading an image and updating the UI based on the background task's status.

Conclusion

This Android Kotlin example demonstrates how to use WorkManager with constraints to handle background tasks efficiently. The app ensures that the download process only starts when specific conditions, like charging and network connectivity, are met, improving battery life and user experience. By observing the task’s progress with live data, the app updates the UI in real-time, providing feedback to the user about the task's status.

WorkManager offers a robust solution for scheduling deferrable background tasks, making it ideal for operations like downloading files, uploading data, or processing large datasets without interrupting the user experience. This example is a starting point for integrating more complex background tasks into your Android applications.


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 {
            // Create a Constraints object that defines when the task should run
            val downloadConstraints = Constraints.Builder()
                // Device need to charging for the WorkRequest to run.
                .setRequiresCharging(true)
                // Any working network connection is required for this work.
                .setRequiredNetworkType(NetworkType.CONNECTED)
                //.setRequiresBatteryNotLow(true)
                // Many other constraints are available, see the
                // Constraints.Builder reference
                .build()

            // Define the input data for work manager
            val data = Data.Builder()
            data.putString("imageUrl",
                "https://www.freeimageslive.com/galleries/buildings/structures/pics/canal100-0416.jpg")

            // Create an one time work request
            val downloadImageWork = OneTimeWorkRequest
                .Builder(DownloadImageWorker::class.java)
                .setInputData(data.build())
                .setConstraints(downloadConstraints)
                .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"

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...