Reading Time: 13 minutes
In this WorkManager basics tutorial we are going to learn about how we can create one time request and chain. It means, that the work will be fired only once until it finishes successfully, then the next work will be started in thet case.
First, we gonna talk about how we can fire only one WorkRequest. Then we are going to create some more worker classes which will be chained after each other.
What is WorkManager?
WorkManager is part of the Android Jetpack library. It runs deferrable guaranteed backround work.
A deferrable task is one that doesn’t need to be executed immediately and may depend upon some constraints such as network conditions, storage space, and charging status.
A work can have different constraints, which determines the conditions when the work can be executed. A WorkManager’s work runs in the background even if the app is closed.
WorkManager has support for one time and periodic asnychronous execution as well.
Periodic execution means, that the next task well be fired when the previous task is successfully finished.
In the consciousness of these conditions, we can see thet WorkManager provides a battery-friendly API.
Battery-friendly, because it executes the tasks when it is the best to save the life of the battery avoiding unncessery wake-up for the phone. This is realy important for Android applications that need to execute background tasks!
When to use WorkManager?
When we need to do a long running task. This task can be started in the background, or also when the app is in the foreground, but needs to be done in the background, or during the execution the Android system kills its process.
WorkManager is not about run in a background thread. For this use cases you should use Kotlin Coroutines or libraries like RxJava.
The sample app
In this tutorial we are going to create a very simple application. Using the application we can see the status of the ordered pizza.
The ordering process gonna have 4 phases.
-
-
- Preparation
- Cooking
- Packing
- Delivery
-
These phases won’t do anything, just wait for few seconds to simulating the duration of the activity. When a phase is done, then the next one will be started automatically.
We are going to have 2 Activities. The first will contain only an “Order Pizza” button. The second Activity will show the current status of the order using a circular ProgressBar and a TextView. Under them we gonna have one more TextView, wich will show us where the phases were fulfilled.
Step 1 – Create new project
First thing first, we gonna create a whole new project. For this, launch Android Studio. If you see the “Welcome page”, then click on the “Start a new Android Studio project”. If you have an open project, then from the “File” menu select “New”, then “New Project”. After thet, this window will popup.
Here select the “Empty Activity” option. In the next window, we should provide some information about the newly created app.
This tutorial will contain very few code, but still we have to select the programming language. As the other tutorials on this website, this will be written also in Kotlin. So, select from the dropdown list the Kotlin language.
From the next list of the “Minimum SDK” select API 23. In our case API 23 gonna be enough.
If you are done, click on the “Finish” button. The build can take few minutes. Be patient!
When the build has been finished, then you should see the open MainActivity::class and next to this the activity_main.xml files.
Step 2 – Add a dependency
In our sample pizza delivery application we are going to create worker classes. Inside of these classes we can create output data. These data can be requested from the Activity in form of an observable LiveData. Because of this we have to add the library for the lifecycle extension of the AndroidX as well.
Next, paste the below implementations into the dependencies {} section.
implementation "androidx.work:work-runtime-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
The needed dependencies
JVM target
The WorkManager library targets the version 1.8 of Java Virtual Machine. So, we have to tell for Android Studio that we would like to use the version 1.8. To do this, stay in the App Build.gradle file and paste the below lines into the end of the android {} section.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
JVM version 1.8
Thenafter click on the Sync now button, what you can find in Android Studio at the top left corner.
Step 3 – The colors and strings
After adding the needed dependencies, we are going to continue the creation by adding to the app some new colors and strings.
strings.xml
We will start first with the strings.xml file. You can find it in the res -> values folder.
Paste into the xml file the below lines, which are the strings of the for phases and a counter four the current state.
<string name="fourPhase">%1$d / 4</string>
<string name="Preparation">Preparation</string>
<string name="Cooking">Cooking</string>
<string name="Packing">Packing</string>
<string name="Delivery">Delivery</string>
<string name="order_pizza">Order pizza</string>
The strings
colors.xml
Thenafter add the below line to the colors.xml file, what you can find also in the res -> values folders.
<color name="colorPrimary">#0090FF</color>
<color name="colorPrimaryDark">#0090FF</color>
<color name="colorAccent">#03DAC5</color>
<color name="lightGreen">#CEEDE4</color>
<color name="darkGreen">#5AD5B0</color>
<color name="light_brown">#F9F4EA</color>
<color name="gray">#F1F1F1</color>
The colors
Step 4 – The UI of MainActivity
For the MainActivity we will have a very simple user interface, because it will contain a rounded button with a nice blue background.
The shape of the button
For this, we will create a new shape, what will be an xml file. So, click with the right mouse button on the res -> drawable folder. Then from the popup window select the New thenafter the Drawable Resource File option. In the popup window name the new file as “shape_roundedcorners_blue”.
After thet, paste the below line into the shape xml file.
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="@color/colorPrimary"/>
<corners
android:radius="48dp"/>
</shape>
The shape of the button
The user interface
Finally open up the activity_main.xml file from the res -> layout folders and switch to Code or Split mode to see the xml source code of the file.
By clicking on this button the second Activity will be started. This second Activity will show us the current state of the ordered pizza.
Currently, there we have a TextView. So, replace it with the below Button.
<Button
android:id="@+id/btn_orderPizza"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/order_pizza"
android:background="@drawable/shape_roundedcorners_blue"
android:textColor="@color/gray"
android:padding="48dp"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
The order button
Step 5 – Customize the ProgressBar
Before we gonna create the OrderingActivity, we have to create 2 new drawable xml files for the background of the ProgressBar and the drawable for its progress line.
shape_circle.xml
Just click on the drawable folder inside of the res folder with the right mouse button, select New and the Drawable resource file. Name it as “shape_circle.xml”. Theafter paste the below line into the xml file.
This shape is risponsible for the ring look for the ProgressBar.
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:innerRadiusRatio="2.5"
android:shape="ring"
android:thickness="7dp"
android:useLevel="false">
<solid android:color="@color/light_brown" />
</shape>
shape_circle
circular_progress_bar.xml
Next xml file will be responsible for the shape and the color of the progressline. So, in the same way, create a new Drawable resource file in the drawable folder with the name of “circular_progress_bar.xml”.
Below you can see the xml code for the circular_progress_bar.xml file.
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="270"
android:toDegrees="270">
<shape
android:innerRadiusRatio="2.5"
android:shape="ring"
android:thickness="7dp"
android:useLevel="true">
<gradient
android:angle="0"
android:endColor="@color/darkGreen"
android:startColor="@color/darkGreen"
android:type="sweep"
android:useLevel="false"/>
</shape>
</rotate>
circular_progress_bar.xml
Step 6 – Create the OrderingActivity
When we click on the Order Pizza Button on the MainActivity, then the OrderingActivity will be opened. In this activity the ordering process gonna be started automatically, when the activity is opens. It means, thet we will fire the first WorkerRequest in the onCreate() method of the OrderingActivity.
To create a new Activity, click on the main source set with the right mouse button, select the New option from the popup menu, then move your mouse to the Activity, and from the submenu select the Empty Activity option. In the popup window name the new activity as “OrderingActivity”.
After the creation Android Studio will generate for us the OrderingActivity::class and the activity_ordering.xml file as well. Next, we need the activity_ordering.xml, because we gonna build up the user interface.
The UI of the OrderingActivity contains a Progressbar, which shows the current status of the order. Inside of it, we gonna show for the user in numeric form the current and the all phases. Below the Progressbar, we will notify the user when the phase has been finished.
Below you can see the xml code for the activity_ordering.xml file.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".OrderingActivity">
<ProgressBar
android:id="@+id/pb_item_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@drawable/shape_circle"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/circular_progress_bar"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 / 4"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/pb_item_progress"
app:layout_constraintLeft_toLeftOf="@+id/pb_item_progress"
app:layout_constraintRight_toRightOf="@+id/pb_item_progress"
app:layout_constraintTop_toTopOf="@+id/pb_item_progress"/>
<TextView
android:id="@+id/tv_orderStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
tools:text="Preparation"
android:textAlignment="center"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pb_item_progress"/>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_ordering.xml
Step 7 – Utils
2 more steps before we can create our first Worker class. We need to define some constants and an Enum class for the states of the ordering process.
Later on we are going to send data from the Worker classes to the OrderingActivity using a LiveData. That’s why we need both, the constants and the enum as well.
First, create a new package called “utils” in the main source set.
Constants
Next, create a new Kotlin class in the utils folder, and name it as “Constants”. Thenafter paste into it the below String constants.
const val TAG_OUTPUT = "OUTPUT"
const val PREPARATIONWORKER = "PreparationWorker"
const val COOKINGWORKER = "CookingWorker"
const val PACKIGNWORKER = "PackingWorker"
const val DELIVERYWORKER = "DeliveryWorker"
const val ORDERING_WORK_NAME = "ordering_work"
Constants.kt
DeliveryState Enum class
The second file will be the DeliveryState::enum also in the utils folder. So, create it as we have done in case of the Constants.kt file, but now be careful to create an Enum class.
The Enum class should look like below.
enum class DeliveryState
{
PREPARATION,
COOKING,
PACKING,
DELIVERY
}
DeliveryState::enum
Step 8 – The workers
To let WorkManager do some tasks, we have to create a new class, which extends the Worker::class. When we did it, then we have to override the doWork() method. What is inside of this method, will be executed when we fire the class. The doWork() method runs synchronously on a background thread provided by WorkManager.
The doWork() method return a Result object, which informs the WorkManager service whether the work succeeded. In case of failure, the work should be retried. So, the Result object has 3 methods.
-
-
- Result.success() : The work finished successfully.
- Result.failure() : The work failed.
- Result.retry() : If the work failed, then the work should be retried in a later time.
-
So, as we have done in case of the utils package, we are going to create a new package for the worker classes as well. So, create it, and name it as “workers”. Inside of this package we are going to create the 4 worker classes.
Create the worker classes
Inside of the workers package create 4 Kotlin files with the below names:
-
-
- CookingWorker
- DeliveryWorker
- PackingWorker
- PreparationWorker
-
After the creation, extend the classes by Worker.
For example the header of the CookingWorker::class should look like below.
class CookingWorker(
context: Context, params: WorkerParameters
) : Worker(context, params)
CookingWorker::class
Note thet the classes takes the Context and the WorkerParameters in the constructor, which are passed into the Worker::class.
Thenafter you have to implement in all classes the doWork() method. In our pizza delivery sample app this method will contain only a CoroutineScope, which has a delay method with few seconds to simulate the long running task inside of the worker.
Beside the delay, the Workers gonna send an output Data object, what we will catch later on using LiveData.
When a worker in the chain failed, then it can be retried in a later time.
To catch a failed WorkRequest you should wrap the code with a try-catch block. When the method failed, then it will jump to the catch part and there WorkManager gonna retry the worker.
In our case we won’t do anything in the worker classes what can be failed, so we won’t implement any try-catch blocks.
After this introduction, your Worker classes should look like below.
PreparationWorker
class PreparationWorker (
context: Context, params: WorkerParameters
) : Worker(context, params)
{
override fun doWork(): Result
{
runBlocking {
delay(2_000)
}
Log.d(TAG_OUTPUT, "PreparationWorker")
val outputData = workDataOf(PREPARATIONWORKER to DeliveryState.PREPARATION.name)
return Result.success(outputData)
}
}
PreparationWorker::class
CookingWorker
class CookingWorker(
context: Context, params: WorkerParameters
) : Worker(context, params)
{
override fun doWork(): Result
{
runBlocking {
delay(5_000)
}
Log.d(TAG_OUTPUT, "CookingWorker")
val outputData = workDataOf(COOKINGWORKER to DeliveryState.COOKING.name)
return Result.success(outputData)
}
}
CookingWorker::class
PackingWorker
class PackingWorker(
context: Context, params: WorkerParameters
) : Worker(context, params)
{
override fun doWork(): Result
{
runBlocking {
delay(3_000)
}
Log.d(TAG_OUTPUT, "PackingWorker")
val outputData = workDataOf(PACKIGNWORKER to DeliveryState.PACKING.name)
return Result.success(outputData)
}
}
PackingWorker::class
DeliveryWorker
class DeliveryWorker(
context: Context, params: WorkerParameters
) : Worker(context, params)
{
override fun doWork(): Result
{
runBlocking {
delay(7_000)
}
Log.d(TAG_OUTPUT, "DeliveryWorker")
val outputData = workDataOf(DELIVERYWORKER to DeliveryState.DELIVERY.name)
return Result.success(outputData)
}
}
DeliveryWorker::class
Note the workDataOf() method before the doWork() methods return. Using this method we can set a Data objects, what will be the output of the Worker classes.
Input and output is passed in and out via Data objects. Data objects are lightweight containers for key/value pairs. They are meant to store a small amount of data that might pass into and out from WorkRequests.
Step 9 – The ViewModel
The next step is to create a ViewModel class. This class will contain the 4 LiveData and the order() method, which will be responsible to define the one time request and later on the chains for the delivery.
Create the DeliveryViewModel
As we have done it before, we are going to create a new package in the main source set for the ViewModel as well. So, create it with the name of “viewmodel”. When it is done, then create inside of this new package a new Kotlin file as well, and name it as “DeliveryViewModel”.
Thenafter extend the class with a constructor. The DeliveryViewModel will get the Application instance. Next, extend the class by AndroidViewModel, which will get the Application instance from the constructor.
Finally, the DeliveryViewModel should look like below.
class DeliveryViewModel(
application: Application
) : AndroidViewModel(application)
{
}
DeliveryViewModel::class
WorkInfo
Next, we have to talk about the WorkInfo object. It is an object, which contains details about the current state of the running WorkRequest. It contains
-
-
- the status: BLOCKED, CANCELLED, ENQUEUED, FAILED, RUNNING or SUCCEEDED
- When the Worker succeeded, then the output data from the work.
-
The 4 LiveData will contain these 4 WorkInfos of the 4 Workers. It means in our case, thet these LiveDatas will notify the OrderingActivity when the Worker reached a new status. When it has reached the SUCCEEDED state, then we can get the Data object, what we have passed inside of the doWork() methods.
Paste the below line into the DeliveryViewModel::class.
// 1
private val workManager = WorkManager.getInstance(application)
// 2
internal val outputWorkInfos_prep: LiveData
internal val outputWorkInfos_cook: LiveData
internal val outputWorkInfos_pack: LiveData
internal val outputWorkInfos_del: LiveData
// 3
init {
outputWorkInfos_prep = workManager.getWorkInfosByTagLiveData(PREPARATIONWORKER)
outputWorkInfos_cook = workManager.getWorkInfosByTagLiveData(COOKINGWORKER)
outputWorkInfos_pack = workManager.getWorkInfosByTagLiveData(PACKIGNWORKER)
outputWorkInfos_del = workManager.getWorkInfosByTagLiveData(DELIVERYWORKER)
}
Create the LiveData objects
-
-
- Create an instance about the WorkManager::class using the instance of the Application from the constructor.
- The new LiveData instance variables for the WorkInfo‘s.
- The init block to initialize the LiveData variables using the getWorkInfosByTagLiveData() method. This method will request the output data from the worker classes.
-
Step 10 - Our first WorkRequest
First, we are going to fire only the PreparationWorker to see how we can create a WorkRequest only for one worker. Later on we gonna learn also how we can chain it with the next phases of the pizza delivery.
So, add the below method to the end of the DeliveryViewModel::class.
internal fun order()
{
// 1
val prepartionBuilder = OneTimeWorkRequestBuilder<PreparationWorker>()
.addTag(PREPARATIONWORKER)
.build()
// 2
var orderingProcess = workManager
.beginUniqueWork(
ORDERING_WORK_NAME,
ExistingWorkPolicy.KEEP,
prepartionBuilder
)
// 3
orderingProcess.enqueue()
}
order()
-
-
- Using the OneTimeWorkRequestBuilder class we can create the WorkRequest. With the addTag() method we can add to the WorkRequest a tag. This is important, because later on we will use it to identify the output data from the Worker.
- Using the beginUniqueWork() method we can start the unique work of the prepartionBuilder.
About the ExistingWorkPolicy.KEEP we gonna talk after this explanation. - We should just enqueue to fire up the WorkRequest.
-
Using the ExistingWorkPolicy we can tell for the WorkManager what it should do with the still running WorkRequests. In our case we gonna use the KEEP option. It means, if we have an existing pending pizza order work, then during the order we can’t order one more pizza. Sorry 😊
If you would like to know more about the rest existing work policies, then check out the below link.
The Observer
Now we have defined the LiveDatas and a WorkRequest. The next step is to fire up this WorkRequest and observe its output data in the DeliveryActivity::class.
So, open up this activity. Then add the below member variable before the onCreate() method.
private lateinit var deliveryViewModel: DeliveryViewModel
Thenafter we gonna init the deliveryViewModel variable in the onCreate() method.
deliveryViewModel = ViewModelProvider(this)
.get(DeliveryViewModel::class.java)
init the deliveryViewModel
Next is to setup the Observer for the outputWorkInfos_prep LiveData. So, paste the below line to the end of the onCreate() method.
deliveryViewModel.outputWorkInfos_prep
.observe(this, deliveryObserver())
The Observer
Now you should have an error, because we haven’t implemented the deliveryObserver() method. We solve it now, so paste the below method after the onCreate() method.
private fun deliveryObserver() : Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
val workInfo = listOfWorkInfo[0]
val data = workInfo.outputData
if (!data.getString(PREPARATIONWORKER).isNullOrEmpty())
{
setOrderStatus(1f, getString(R.string.Preparation))
}
}
}
deliveryObserver()
Oh, one more red error?! 🤔
Yes, because we need one more method, which gonna update the pizza order’s status. So, paste the below method after the deliveryObserver() method.
private fun setOrderStatus(phase: Float, phaseName: String)
{
// 1
pb_item_progress.progress = ((phase / 4) * 100).toInt()
// 2
tv_steps.text = String.format(getString(R.string.fourPhase), phase.toInt())
// 3
tv_orderStatus.append("$phaseName\n${Calendar.getInstance().time.formatTime()}\n\n")
}
setOrderStatus()
-
-
- Set the progress of the ProgressBar by calculating the percentage.
- Set the text of the tv_steps‘s TextView to show the current phase.
- Update the TextView with the order’s history.
-
Ok ok, I promise, this will be the last error in our application. 😀
The formatTime() method will be an extension function of the Date class. This function will give back a String with the time, when the phase of the pizza order had been finished.
Kotlin provides the ability to extend a class with new functionality without having to inherit from the class or use design patterns such as Decorator. This is done via special declarations called extensions.
Source: Kotlin Extensions
Paste the below extension function after the DeliveryActivity::class.
fun Date.formatTime() : String
{
val pattern = "HH:mm:ss"
val simpleDateFormat = SimpleDateFormat(pattern)
return simpleDateFormat.format(this)
}
formatTime()
Step 11 - Start the OrderingActivity
Before we can run the app, we have to add a click listener for the Order Pizza Button in the MainActivity::class, otherwise our app won’t do anything. So, open the MainActivity::class from the main source set, and add the below method after the onCreate() method.
private fun navigateToOrderActivity()
{
val intent = Intent(this, OrderingActivity::class.java)
startActivity(intent)
}
navigateToOrderActivity()
Then call this method from the click listener of the Order Pizza Button.
btn_orderPizza.setOnClickListener {
navigateToOrderActivity()
}
setOnClickListener
Run the app
Finally, it is time to run the app.
When you click on the Order Pizza Button, then the app will navigate you to the OrderingActivity and the order process will be started automatically.
Currently only the first phase is fired, so let’s see how we can chain to it the rest phases.
Step 12 - Chain the workers
In this step we are going to chain the workers of the 4 phases of our pizza ordering system. So, basically, the phases have to be started when the previous phase is finished successfully.
Extend the DeliveryViewModel
Now, open the DeliveryViewModel::class from the viewmodel package and extend the order() method with the below lines. These lines should be placed before the orderingProcess.enqueue() line, but after the creation of the orderingProcess variable.
val cookingBuilder = OneTimeWorkRequestBuilder<CookingWorker>()
.addTag(COOKINGWORKER)
.build()
orderingProcess = orderingProcess.then(cookingBuilder)
val packingBuilder = OneTimeWorkRequestBuilder<PackingWorker>()
.addTag(PACKIGNWORKER)
.build()
orderingProcess = orderingProcess.then(packingBuilder)
val deliveryBuilder = OneTimeWorkRequestBuilder<DeliveryWorker>()
.addTag(DELIVERYWORKER)
.build()
orderingProcess = orderingProcess.then(deliveryBuilder)
The phases of the ordering system
Note thet the OnTimeRequests are the same like what we have used for the preparation phase.
The more important thing is the then() method. Using this method we can attach a worker to the WorkManager. In this case it specifies the order of the workers, because they gonna be fired in in sequential order.
Extend the OrderingActivity
The next step is to add the Observers for the rest phases in the DeliveryActivity::class as well. So, open it from the main source set and add the below Observers to the onCreate() method.
deliveryViewModel.outputWorkInfos_cook
.observe(this, deliveryObserver())
deliveryViewModel.outputWorkInfos_pack
.observe(this, deliveryObserver())
deliveryViewModel.outputWorkInfos_del
.observe(this, deliveryObserver())
The observers
Finally, extend the if-statement with the below else-if-statements in the deliveryObserver() method. These lines will catch the WorkInfo objects from the rest 3 Workers as well. The user interface will be notified based on these WorkInfos.
else if (!data.getString(COOKINGWORKER).isNullOrEmpty())
{
setOrderStatus(2f, getString(R.string.Cooking))
}
else if (!data.getString(PACKIGNWORKER).isNullOrEmpty())
{
setOrderStatus(3f, getString(R.string.Packing))
}
else if (!data.getString(DELIVERYWORKER).isNullOrEmpty())
{
setOrderStatus(4f, getString(R.string.Delivery))
}
Catch the WorkInfos
Run the app
Run again the app and test it. It should work as you can see on the video below.
GitHub
The source code is available on GitHub, check it out and download it using the below link.
Questions
I hope the description was understandable and clear. But, if you have still questions, then leave me comments below! 😉
Have a nice a day! 🙂