Android Kotlin: WorkManager example

Introduction

In this article, we explore how to use Android's WorkManager with Kotlin to perform background tasks such as downloading images. WorkManager is part of Android Jetpack and is designed for tasks that require guaranteed execution, whether or not the app is in the foreground. It is particularly useful for scenarios like periodic data syncing, large file downloads, and applying long-running operations that need to be preserved even after app termination.

We'll walk through an example that demonstrates how to download an image using WorkManager, store it in internal storage, and display it in an Android app. This guide covers the key components such as setting up the WorkManager request, managing background worker tasks, and handling image storage with Kotlin extensions.

Breakdown of MainActivity

The MainActivity is where the core functionality of the app begins. In the onCreate method, the layout is initialized, which includes two buttons: one for downloading an image (btnDownload) and one for displaying the downloaded image (btnShow). When the "Download Image" button is clicked, a one-time WorkManager request is created and enqueued. This request triggers the DownloadImageWorker class, which handles the task of downloading the image from a specified URL.

The second button, "Show Image," retrieves the most recently downloaded image from internal storage and displays it in an ImageView. This is done using Kotlin extension functions to count and list saved images in internal storage. If no images have been downloaded yet, a message is displayed using a custom toast function.

Worker Class for Downloading the Image

The DownloadImageWorker class extends Worker and is responsible for the background operation of downloading the image from the internet. The doWork() method performs the core task of opening a network connection, fetching the image, and converting it into a Bitmap object.

To ensure the operation works smoothly, the app needs internet permission in the manifest. Once the image is successfully downloaded, it is saved to internal storage using a custom Kotlin extension function (saveToInternalStorage). The result of the work is then logged and returned, with either Result.success() or Result.failure() depending on whether the image was downloaded successfully.

Bitmap Extension Functions

The xBitmap.kt file contains several useful Kotlin extension functions that enhance the handling of images within the app. One of the key functions is saveToInternalStorage, which takes a Bitmap object and saves it to the app's internal storage. It also returns a Uri pointing to the saved file, which is later used to display the image in the app. The image is stored in a directory named "images" within the app's private storage space.

Other extension functions include countSavedBitmap, which returns the number of images stored in internal storage, and savedBitmapList, which provides a list of all the saved images. The nameToUri function helps convert a file name into a Uri, which is necessary for displaying the image in the app's ImageView.

UI Design in XML

The user interface of the app is defined in the activity_main.xml file. The layout is a simple ConstraintLayout with two buttons (btnDownload and btnShow), an ImageView to display the downloaded image, and a TextView that shows the URI of the image. The buttons are set up with onClick listeners in MainActivity to trigger the image download and display actions, respectively.

Gradle Dependencies

To use WorkManager, the app includes a dependency in its build.gradle file. This dependency ensures that WorkManager is available in the app, allowing us to manage background tasks reliably. The specific version used in this example is 1.0.0-beta02, but developers should check for the latest stable version when implementing WorkManager in their own projects.

Summary

This Android Kotlin example showcases how to efficiently use WorkManager to perform background operations such as downloading images and saving them to internal storage. By utilizing Kotlin extension functions, the app simplifies the process of image handling, making it easier to store, retrieve, and display downloaded images. WorkManager provides a powerful way to manage long-running tasks that need guaranteed execution, even when the app is not running.

In conclusion, this example demonstrates the seamless integration of WorkManager for background tasks, Kotlin's extension functions for enhanced code readability, and an efficient UI layout for a user-friendly experience. This approach can be extended to various use cases where background processing and file handling are required in 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 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))
        }


        // Display the downloaded image
        btnShow.setOnClickListener{
            if (countSavedBitmap>0){
                imageView.setImageURI(nameToUri(savedBitmapList[countSavedBitmap-1]))
                textView.text = savedBitmapList[savedBitmapList.count()-1]
            }else
            {
                toast("No downloaded image found")
            }
        }
    }
}


// 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/buildings/china/pics/nature_water3978.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? {
    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"/>
    <Button
            android:text="Show Image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnShow"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnDownload"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="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="Image URI"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/textView"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnShow" 
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
gradle dependencies

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