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:

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:

  1. It has a BindingAdapter that can update the enabled status of a view given a WorkInfo, allowing us to use a binding expression on android:enabled
  2. It binds the DownloadViewModel into the binding
  3. It calls setLifecycleOwner() on the binding, which the data binding framework will use for observing the LiveData:
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.