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"