Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Threading in ListenableWorker

In certain situations, you may need to provide a custom threading strategy. For example, you may need to handle a callback-based asynchronous operation. In this case, you cannot simply rely on a Worker because it can't do the work in a blocking fashion. WorkManager supports this use case with ListenableWorker. ListenableWorker is the lowest-level worker API; Worker, CoroutineWorker, and RxWorker all derive from this class. A ListenableWorker only signals when the work should start and stop and leaves the threading entirely up to you. The start work signal is invoked on the main thread, so it is very important that you go to a background thread of your choice manually.

The abstract method ListenableWorker.startWork() returns a ListenableFuture of the Result. A ListenableFuture is a lightweight interface: it is a Future that provides functionality for attaching listeners and propagating exceptions. In the startWork method, you are expected to return a ListenableFuture, which you will set with the Result of the operation once it's completed. You can create ListenableFutures one of two ways:

  1. If you use Guava, use ListeningExecutorService.
  2. Otherwise, include councurrent-futures in your gradle file and use CallbackToFutureAdapter.

If you wanted to execute some work based on an asynchronous callback, you would do something like this:

public class CallbackWorker extends ListenableWorker {

    public CallbackWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            Callback callback = new Callback() {
                int successes = 0;

                @Override
                public void onFailure(Call call, IOException e) {
                    completer.setException(e);
                }

                @Override
                public void onResponse(Call call, Response response) {
                    ++successes;
                    if (successes == 100) {
                        completer.set(Result.success());
                    }
                }
            };

            for (int i = 0; i < 100; ++i) {
                downloadAsynchronously("https://www.google.com", callback);
            }
            return callback;
        });
    }
}

What happens if your work is stopped? A ListenableWorker's ListenableFuture is always cancelled when the work is expected to stop. Using a CallbackToFutureAdapter, you simply have to add a cancellation listener, as follows:

public class CallbackWorker extends ListenableWorker {

    public CallbackWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            Callback callback = new Callback() {
                int successes = 0;

                @Override
                public void onFailure(Call call, IOException e) {
                    completer.setException(e);
                }

                @Override
                public void onResponse(Call call, Response response) {
                    ++successes;
                    if (successes == 100) {
                        completer.set(Result.success());
                    }
                }
            };

            completer.addCancellationListener(cancelDownloadsRunnable, executor);

            for (int i = 0; i < 100; ++i) {
                downloadAsynchronously("https://www.google.com", callback);
            }
            return callback;
        });
    }
}