Introduction
WorkManager is a library in Android Jetpack that allows developers to schedule deferrable, asynchronous tasks that are guaranteed to execute even if the app exits or the device is restarted. This makes it highly suitable for background tasks that require guaranteed execution, such as downloading images, syncing data, or sending logs. In this article, we’ll explore an example of using WorkManager in Kotlin to download an image from the internet, manage input and output data, and display the result on the screen. This example showcases how WorkManager simplifies task scheduling and ensures task completion even in challenging circumstances.
In addition to providing an overview of the WorkManager implementation, this breakdown will delve into key components such as input data, output data, observing task states, and handling background processes efficiently. By the end of this walkthrough, you'll have a good understanding of how to implement WorkManager for tasks requiring data transfer and persistent task execution in Android.
MainActivity Overview
The heart of this example lies in MainActivity.kt
, where the interaction with the user is handled. The layout features a button (btnDownload
) that initiates the process, a TextView
to show the status of the download, and an ImageView
to display the downloaded image.
When the user clicks the "Download Image" button, it triggers a WorkManager task. The first step is defining the input data needed for the task. In this case, the input is a URL string (imageUrl
) for the image to be downloaded. This input data is bundled into a Data.Builder
object and passed to the WorkManager task. The task itself is represented by a OneTimeWorkRequest
, which is designed to run only once.
Once the WorkRequest is created, it is enqueued using WorkManager.getInstance().enqueue()
. This schedules the task to be executed by WorkManager. At the same time, LiveData is used to observe the status of the task through getWorkInfoByIdLiveData()
. The state of the task, whether enqueued, running, or finished, is displayed in the TextView
. Additionally, a toast notification is triggered to reflect the current state.
WorkManager and Worker Class
The image download process is handled in a background thread by the DownloadImageWorker
class, which extends Worker
. The doWork()
method contains the core logic for downloading the image and saving it to internal storage.
The doWork()
method begins by retrieving the imageUrl
passed via the input data. This URL is then converted into a HttpURLConnection
object, which establishes a connection to the server. Once the connection is made, the input stream of data is retrieved and converted into a Bitmap
using BitmapFactory.decodeStream()
. This Bitmap
object represents the downloaded image.
If the image is successfully downloaded, it is saved to the internal storage of the app using the custom saveToInternalStorage()
extension function. The saved file's URI is then returned as part of the output data, indicating a successful task completion. If the download fails for any reason, an error is logged, and a failure result is returned.
Input and Output Data
Input and output data are crucial parts of WorkManager tasks. In this example, input data is used to pass the URL of the image to the worker. The Data.Builder
class allows you to pass key-value pairs to the worker, making it easy to handle dynamic data.
The output data is generated in the doWork()
method after the image is successfully downloaded and saved. The createOutputData()
method creates a Data
object containing the URI of the saved image. This output data is then accessed in the MainActivity
using LiveData. Once the worker has finished and if the task is successful, the ImageView
displays the downloaded image using the URI provided in the output data.
Handling Task States
The application uses LiveData to monitor the state of the WorkManager task. WorkManager provides several states, including ENQUEUED
, RUNNING
, SUCCEEDED
, FAILED
, and CANCELLED
. These states help in providing feedback to the user during various stages of task execution.
For example, when the task is enqueued, the TextView
updates to show "Download enqueued." If the task is blocked or in progress, corresponding messages are displayed. Finally, when the task completes, either successfully or with failure, the UI is updated accordingly. If the task succeeds, the image is shown in the ImageView
, and if it fails, an error message is displayed.
Bitmap Saving and Utilities
The custom extension function saveToInternalStorage()
in xBitmap.kt
handles the task of saving the downloaded bitmap to the device’s internal storage. It creates a unique file name using UUID.randomUUID()
, compresses the bitmap, and writes it to an output stream. The function returns the URI of the saved file, which is then passed back to the main activity via output data.
Additionally, the helper function stringToURL()
is used to convert the URL string into a proper URL
object. This is a simple utility to handle the potential MalformedURLException
that could arise during string-to-URL conversion.
Summary
This Android Kotlin example demonstrates how WorkManager can be used to manage background tasks such as downloading images, handling input and output data, and updating the UI based on task states. The separation of concerns is clear, with the MainActivity
managing UI updates and user interactions, while the DownloadImageWorker
handles the background task of image downloading and saving.
By using WorkManager, the application ensures that the task will complete, even if the app is closed or the device is restarted, making it a robust solution for deferred tasks in Android development. This example provides a foundation for building more complex background tasks using WorkManager in Android.
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 {
// Define the input data for work manager
val data = Data.Builder()
data.putString("imageUrl",
"https://www.freeimageslive.com/galleries/food/breakfast/pics/muffin1964.jpg")
// Create an one time work request
val downloadImageWork = OneTimeWorkRequest
.Builder(DownloadImageWorker::class.java)
.setInputData(data.build())
.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()
}
}
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()
<?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>
// WorkManager
def work_version = "1.0.0-beta03"
implementation "android.arch.work:work-runtime:$work_version"