Introduction
WorkManager is an Android architecture component that provides a powerful and flexible API for scheduling background tasks. It allows developers to enqueue tasks that can be guaranteed to execute, even if the app is killed or the device is rebooted. In this article, we will explore how to use WorkManager in a Kotlin Android app, with an example that demonstrates how to download an image from the web and display it in the app.
This example demonstrates the use of WorkManager's OneTimeWorkRequest
to download images asynchronously, passing URL parameters to the worker class. The worker performs the download operation, saves the image to internal storage, and the app later retrieves and displays it.
MainActivity and User Interaction
In the MainActivity.kt
file, the app's layout is set in the onCreate()
method. Two buttons are used for interacting with the user: one for downloading an image (btnDownload
) and another for displaying the downloaded image (btnShow
). The user inputs the image URL into a text field (editText
), and the Download Image
button initiates the image download process.
Before starting the download, the code validates the URL using URLUtil.isValidUrl()
. If the URL is valid, the image URL is packed into a Data
object and passed as input data to the WorkManager
. This input data allows the worker to access the URL inside its doWork()
method.
The OneTimeWorkRequest
is then created and enqueued by calling WorkManager.getInstance().enqueue()
. This ensures that the task will be scheduled for execution. If the URL is invalid, the user receives a toast message informing them of the error.
WorkManager and Image Downloading
The actual downloading of the image happens in the DownloadImageWorker
class, which extends the Worker
class provided by the WorkManager API. The doWork()
method is responsible for fetching the image from the URL passed to it in the input data. The URL is converted to a URL
object using the helper function stringToURL()
.
A HttpURLConnection
is used to establish the connection to the URL. The image is downloaded as a stream and decoded into a Bitmap
object using BitmapFactory.decodeStream()
. After successfully downloading the image, the Bitmap
is saved to the device's internal storage using an extension function, saveToInternalStorage()
. If the download or connection fails, appropriate error handling is in place to log the error and return a Result.failure()
.
Saving and Retrieving the Image
The Bitmap.saveToInternalStorage()
function is an extension function that handles saving the downloaded image to the device's internal storage. It uses the ContextWrapper
class to get the internal storage directory and saves the image as a PNG file with a unique name generated by UUID.randomUUID()
.
In addition to saving images, two more extension properties are defined: countSavedBitmap
, which returns the number of saved images, and savedBitmapList
, which provides a list of image filenames stored in the internal storage. These properties are used to retrieve and display the most recently saved image when the user clicks the Show Image
button in the app.
Displaying the Image
When the user presses the Show Image
button, the app checks if any images have been downloaded by evaluating countSavedBitmap
. If images are found, the last saved image is retrieved and displayed in the ImageView
by calling nameToUri()
to convert the saved filename into a URI. The URI is then set as the source of the ImageView
, and the image's filename is displayed in the TextView
below the image.
If no images are available, the user is informed via a toast message that no images have been downloaded.
Summary
This Kotlin Android app demonstrates a practical use case for WorkManager by downloading images in the background, passing input parameters such as a URL, and handling the downloading task in a worker. The image is saved to internal storage, and the app retrieves and displays the image when requested by the user.
WorkManager provides a simple and reliable way to handle background tasks that need to continue even if the app is closed. Its ability to pass parameters, handle retries, and schedule tasks for guaranteed execution makes it a robust tool for background task management in Android development.
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.webkit.URLUtil
import androidx.work.*
import java.net.URL
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Download image
btnDownload.setOnClickListener{
// Test image url
// https://www.freeimageslive.com/galleries/nature/animals/pics/guinea_pig.jpg
// https://www.freeimageslive.com/galleries/nature/animals/pics/hens4949.jpg
val text:String? = editText.text.toString()
if (URLUtil.isValidUrl(text)){
// Define the input data for work manager
val data = Data.Builder()
data.putString("url",editText.text.toString())
// You can pass more data in parameters
//data.putString("url2","https://www.freeimageslive.com/galleries/nature/animals/pics/hens4949.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))
}else{
toast("invalid url")
}
}
// 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 {
// Get the input data from parameters
val urlString:String? = inputData.getString("url")
// You can get the more url as the same way
//val urlString:String? = inputData.getString("url2")
// Do the work here
val url:URL? = stringToURL(urlString)
url?.let {
// 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?.inputStream
// 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_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.454"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout"/>
<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"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="395dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
android:id="@+id/textInputLayout">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/editText"
android:hint="Image URL"
android:text="https://www.freeimageslive.com/galleries/nature/animals/pics/donkey.jpg"
/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
// WorkManager
def work_version = "1.0.0-beta02"
implementation "android.arch.work:work-runtime:$work_version"