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.
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()
}
}
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()
<?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>
// WorkManager
def work_version = "1.0.0-beta03"
implementation "android.arch.work:work-runtime:$work_version"

