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.
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()
}
}
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"