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().getWorkInfoByIdLiveData(downloadWork.getId());

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:

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.android.work.download;

import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
import android.arch.lifecycle.ViewModel;
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 ViewModel {
  public final MediatorLiveData<WorkInfo> liveWorkStatus=new MediatorLiveData<>();

  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().enqueue(downloadWork);

    final LiveData<WorkInfo> liveOpStatus=
      WorkManager.getInstance().getWorkInfoByIdLiveData(downloadWork.getId());

    liveWorkStatus.addSource(liveOpStatus, workStatus -> {
      liveWorkStatus.setValue(workStatus);

      if (workStatus.getState().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 described in the chapter on LiveData and data binding. 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:

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final DownloadViewModel vm=ViewModelProviders.of(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();
      }
    });
  }

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.

To that end, we bind the DownloadViewModel into the binding, as was shown in the chapter on LiveData and data binding. The layout then has binding expressions both for android:onClick and android:enabled on its Button:

<?xml version="1.0" encoding="utf-8"?>
<layout>

  <data>

    <variable
      name="viewModel"
      type="com.commonsware.android.work.download.DownloadViewModel" />
  </data>

  <android.support.constraint.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" />
  </android.support.constraint.ConstraintLayout>
</layout>

android:onClick just calls doTheDownload on the DownloadViewModel. android:enabled takes advantage of the LiveData support in data binding, with the extra assistance of a BindingAdapter:

  @BindingAdapter("android:enabled")
  public static void setEnabled(View v, WorkInfo info) {
    if (info==null) {
      v.setEnabled(true);
    }
    else {
      v.setEnabled(info.getState().isFinished());
    }
  }

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.