Monitoring Work
WorkManager
does not provide a built-in means for you to monitor progress inside of an individual piece of work. It does, however, provide you with an API for monitoring the gross state changes of a piece of work: is it enqueued, is it running, is it completed, etc.
Getting the Status Updates
To find out about the general state changes in the life of a piece of work, you can use getWorkInfoByIdLiveData()
, available on WorkManager
. Each request has an ID, generated by the WorkManager
system, which you get by calling getId()
on the request:
final LiveData<WorkInfo> liveOpStatus =
WorkManager.getInstance(getApplication()).getWorkInfoByIdLiveData(
downloadWork.getId());
val liveOpStatus =
WorkManager.getInstance(getApplication())
.getWorkInfoByIdLiveData(downloadWork.id)
The LiveData
that we get back will emit WorkInfo
updates for the work identified by this ID. A WorkInfo
, in turn, holds a State
enum, that indicates what phase of the WorkManager
process this piece of work is in:
ENQUEUED
-
BLOCKED
(for use with chained work) RUNNING
SUCCEEDED
FAILED
-
CANCELED
(for use with canceling work)
You can then arrange to observe the LiveData
or otherwise make use of its updates.
Consuming the Status Updates… In Code
The code shown in this chapter so far that created the OneTimeWorkRequest
and enqueued the work is in a DownloadViewModel
:
package com.commonsware.jetpack.work.download;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
public class DownloadViewModel extends AndroidViewModel {
public final MediatorLiveData<WorkInfo> liveWorkStatus =
new MediatorLiveData<>();
public DownloadViewModel(@NonNull Application application) {
super(application);
}
public void doTheDownload() {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build();
OneTimeWorkRequest downloadWork =
new OneTimeWorkRequest.Builder(DownloadWorker.class)
.setConstraints(constraints)
.setInputData(new Data.Builder()
.putString(DownloadWorker.KEY_URL,
"https://commonsware.com/Android/Android-1_0-CC.pdf")
.putString(DownloadWorker.KEY_FILENAME, "oldbook.pdf")
.build())
.addTag("download")
.build();
WorkManager.getInstance(getApplication()).enqueue(downloadWork);
final LiveData<WorkInfo> liveOpStatus =
WorkManager.getInstance(getApplication()).getWorkInfoByIdLiveData(
downloadWork.getId());
liveWorkStatus.addSource(liveOpStatus, workStatus -> {
liveWorkStatus.setValue(workStatus);
if (workStatus.getState().isFinished()) {
liveWorkStatus.removeSource(liveOpStatus);
}
});
}
}
package com.commonsware.jetpack.work.download
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.work.*
class DownloadViewModel(application: Application) :
AndroidViewModel(application) {
val liveWorkStatus = MediatorLiveData<WorkInfo>()
fun doTheDownload() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val downloadWork = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
.setConstraints(constraints)
.setInputData(
Data.Builder()
.putString(
DownloadWorker.KEY_URL,
"https://commonsware.com/Android/Android-1_0-CC.pdf"
)
.putString(DownloadWorker.KEY_FILENAME, "oldbook.pdf")
.build()
)
.addTag("download")
.build()
WorkManager.getInstance(getApplication()).enqueue(downloadWork)
val liveOpStatus =
WorkManager.getInstance(getApplication())
.getWorkInfoByIdLiveData(downloadWork.id)
liveWorkStatus.addSource(liveOpStatus) { workStatus ->
liveWorkStatus.value = workStatus
if (workStatus.state.isFinished) {
liveWorkStatus.removeSource(liveOpStatus)
}
}
}
}
The doTheDownload()
method will be called when the user clicks a button in the UI of MainActivity
. That triggers our creation of the work request.
DownloadViewModel
takes the MediatorLiveData
approach seen elsewhere in the book. Consumers of the DownloadViewModel
, such as our MainActivity
, have access to a liveWorkStatus
field that represents the outbound stream of work status updates. For each doTheDownload()
call, we chain the LiveData
for this individual download onto the MediatorLiveData
, removing it as a source once the State
reaches a terminal condition (isFinished()
, which will be true
for a State
of SUCCEEDED
, FAILED
, or CANCELED
).
The result is that our MainActivity
can observe liveWorkStatus
, without having to worry about individual LiveData
objects from individual download requests.
MainActivity
observes liveWorkStatus
and uses it to display a Toast
when the download is finished:
package com.commonsware.jetpack.work.download;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.commonsware.jetpack.work.download.databinding.ActivityMainBinding;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.BindingAdapter;
import androidx.lifecycle.ViewModelProvider;
import androidx.work.WorkInfo;
public class MainActivity extends AppCompatActivity {
@BindingAdapter("android:enabled")
public static void setEnabled(View v, WorkInfo info) {
if (info==null) {
v.setEnabled(true);
}
else {
v.setEnabled(info.getState().isFinished());
}
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final DownloadViewModel vm= new ViewModelProvider(this).get(DownloadViewModel.class);
binding=ActivityMainBinding.inflate(getLayoutInflater());
binding.setViewModel(vm);
binding.setLifecycleOwner(this);
setContentView(binding.getRoot());
vm.liveWorkStatus.observe(this, workStatus -> {
if (workStatus!=null && workStatus.getState().isFinished()) {
Toast.makeText(this, R.string.msg_done, Toast.LENGTH_LONG).show();
}
});
}
}
package com.commonsware.jetpack.work.download
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.BindingAdapter
import androidx.work.WorkInfo
import com.commonsware.jetpack.work.download.databinding.ActivityMainBinding
@BindingAdapter("android:enabled")
fun View.setEnabled(info: WorkInfo?) {
isEnabled = info?.state?.isFinished ?: true
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: DownloadViewModel by viewModels()
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.viewModel = vm
binding.lifecycleOwner = this
setContentView(binding.root)
vm.liveWorkStatus.observe(this) { workStatus ->
if (workStatus != null && workStatus.state.isFinished) {
Toast.makeText(this, R.string.msg_done, Toast.LENGTH_LONG).show()
}
}
}
}
Consuming the Status Updates… In Data Binding
MainActivity
— and its activity_main
layout resource — use data binding. Partially, this is to get control to DownloadViewModel
when the user clicks a button. But we also want to disable the button while the download is going on, to reduce the likelihood of accidentally triggering multiple downloads.
Data binding has dedicated support for LiveData
. If you have a LiveData
available through a <variable>
, you can reference the LiveData
in a binding expression as if it were a simple variable representing the data. The data binding framework will take care of the details of observing the LiveData
and updating your UI when the data changes.
To make this work, our layout has a reference to the DownloadViewModel
and has a binding expression on android:enabled
that looks at the state:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.commonsware.jetpack.work.download.DownloadViewModel" />
</data>
<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=".MainActivity">
<Button
android:id="@+id/download"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/btn_title"
android:onClick="@{() -> viewModel.doTheDownload()}"
android:enabled="@{viewModel.liveWorkStatus }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Our MainActivity
then does three things in support of all of this:
- It has a
BindingAdapter
that can update the enabled status of a view given aWorkInfo
, allowing us to use a binding expression onandroid:enabled
- It binds the
DownloadViewModel
into the binding - It calls
setLifecycleOwner()
on the binding, which the data binding framework will use for observing theLiveData
:
package com.commonsware.jetpack.work.download;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.commonsware.jetpack.work.download.databinding.ActivityMainBinding;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.BindingAdapter;
import androidx.lifecycle.ViewModelProvider;
import androidx.work.WorkInfo;
public class MainActivity extends AppCompatActivity {
@BindingAdapter("android:enabled")
public static void setEnabled(View v, WorkInfo info) {
if (info==null) {
v.setEnabled(true);
}
else {
v.setEnabled(info.getState().isFinished());
}
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final DownloadViewModel vm= new ViewModelProvider(this).get(DownloadViewModel.class);
binding=ActivityMainBinding.inflate(getLayoutInflater());
binding.setViewModel(vm);
binding.setLifecycleOwner(this);
setContentView(binding.getRoot());
vm.liveWorkStatus.observe(this, workStatus -> {
if (workStatus!=null && workStatus.getState().isFinished()) {
Toast.makeText(this, R.string.msg_done, Toast.LENGTH_LONG).show();
}
});
}
}
package com.commonsware.jetpack.work.download
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.BindingAdapter
import androidx.work.WorkInfo
import com.commonsware.jetpack.work.download.databinding.ActivityMainBinding
@BindingAdapter("android:enabled")
fun View.setEnabled(info: WorkInfo?) {
isEnabled = info?.state?.isFinished ?: true
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: DownloadViewModel by viewModels()
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.viewModel = vm
binding.lifecycleOwner = this
setContentView(binding.root)
vm.liveWorkStatus.observe(this) { workStatus ->
if (workStatus != null && workStatus.state.isFinished) {
Toast.makeText(this, R.string.msg_done, Toast.LENGTH_LONG).show()
}
}
}
}
Here, we map the State
from a WorkInfo
to the boolean
value to use for the android:enabled
attribute. Basically, if the WorkInfo
is null
or is finished, the button is enabled, otherwise it is disabled. So, as the LiveData
emits new WorkInfo
objects, data binding takes each, calls this setEnabled()
method, and uses that to update the enabled state of the Button
.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.