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"