Use of WorkManager in Android App (Part-4)

Abhishek Srivastava
7 min readAug 15, 2020

Welcome to the fourth post of our WorkManager series.

In this blog post, I’ll cover:

  1. Custom Work Manager configuration with Types of Worker classes
  2. Work Manager Threading

1. Custom WorkManager Configuration

By default, WorkManager configures itself automatically when your app starts, using reasonable options that are suitable for most apps. If you require more control of how WorkManager manages and schedules work, you can customize the WorkManager configuration by initializing WorkManager yourself.

1.1 WorkManager 2.1.0 and later

WorkManager 2.1.0 has multiple ways to configure WorkManager. The most flexible way to provide a custom initialization for WorkManager is to use on-demand initialization, available in WorkManager 2.1.0 and later. The other options are discussed later.

On-Demand Initialization

On-demand initialization lets you create WorkManager only when that component is needed, instead of every time the app starts up. Doing so moves WorkManager off your critical startup path, improving app startup performance. To use on-demand initialization:

Remove the default initializer

To provide your own configuration, you must first remove the default initializer. To do so, update AndroidManifest.xml using the merge rule tools:node="remove":

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />

To learn more about using merge rules in your manifest, see the documentation on merging multiple manifest files.

Implement Configuration.Provider

Have your Application class implement the Configuration.Provider interface, and providing your own implementation of Configuration.Provider.getWorkManagerConfiguration().

When you need to use WorkManager, make sure to call the method WorkManager.getInstance(Context). WorkManager calls your app's custom getWorkManagerConfiguration() method to discover its Configuration. (You do not need to call WorkManager.initialize() yourself.)

Note: If you call the deprecated no-parameter WorkManager.getInstance() method before WorkManager has been initialized, the method throws an exception. You should always use the WorkManager.getInstance(Context) method, even if you're not customizing WorkManager.

Here’s an example of a custom getWorkManagerConfiguration() implementation:

class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()
}

WorkManager 2.0.1 and earlier

For older versions of WorkManager, there are two initialization options:

Default initialization

In most cases, the default initialization is all you need.

Custom initialization

For more precise control of WorkManager, you can specify your own configuration.

Default initialization

WorkManager uses a custom ContentProvider to initialize itself when your app starts. This code lives in the internal class. androidx.work.impl.WorkManagerInitialize, and uses the default Configuration. The default initializer is automatically used unless you explicitly disable it. The default initializer is suitable for most apps.

Custom initialization

If you want to control the initialization process, you must disable the default initializer, then define your own custom configuration.

Once the default initializer is removed, you can manually initialize WorkManager:

// provide custom configuration
val myConfig = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()

// initialize WorkManager
WorkManager.initialize(this, myConfig)

the WorkManager singleton. Make sure the initialization runs either in Application.onCreate() or in a ContentProvider.onCreate().

2. Work Manager Threading with Kotlin

2.1 Threading in Worker

When you use a Worker, WorkManager automatically calls Worker.doWork() on a background thread. The background thread comes from the Executor specified in WorkManager's Configuration. By default, WorkManager sets up an Executor for you - but you can also customize your own. For example, you can share an existing background Executor in your app, or create a single-threaded Executor to make sure all your background work executes serially, or even specify a ThreadPool with a different thread count. To customize the, make sure you have enabled manual initialization of WorkManager. When configuring WorkManager, you can specify your Executor as follows:

WorkManager.initialize(
context,
Configuration.Builder()
.setExecutor(Executors.newFixedThreadPool(8))
.build())

Here is an example of a simple Worker that downloads the content of some websites sequentially:

class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

override fun doWork(): ListenableWorker.Result {
for (i in 0..99) {
try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
}
}

return ListenableWorker.Result.success()
}
}

Note that Worker.doWork() is a synchronous call - you are expected to do the entirety of your background work in a blocking fashion and finish it by the time the method exits. If you call an asynchronous API in doWork() and return a Result, your callback may not operate properly. If you find yourself in this situation, consider using a ListenableWorker (see Threading in ListenableWorker).

When a currently running Worker is stopped for any reason, it receives a call to Worker.onStopped(). Override this method or call Worker.isStopped() to checkpoint your code and free up resources when necessary. WhenWorker in the example above is stopped, it may be in the middle of its loop of downloading items and will continue doing so even though it has been stopped. To optimize this behavior, you can do something like this:

class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

override fun doWork(): ListenableWorker.Result {
for (i in 0..99) {
if (isStopped) {
break
}

try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
}

}

return ListenableWorker.Result.success()
}
}

Once a Worker has been stopped, it doesn't matter what you return from Worker.doWork(); the Result will be ignored.

2.2 Threading in CoroutineWorker

For Kotlin users, WorkManager provides first-class support for coroutines. To get started, include work-runtime-ktx in your gradle file. Instead of extending Worke, you should extend, which has a slightly different API. For example, if you wanted to build a simple CoroutineWorker to perform some network operations, you would do the following:

class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result = coroutineScope {
val jobs = (0 until 100).map {
async {
downloadSynchronously("https://www.google.com")
}
}

// awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
jobs.awaitAll()
Result.success()
}
}

Note that CoroutineWorker.doWork() is a suspending function. Unlike Worker, this code does not run on the Executor specified in your Configuration. Instead, it defaults to Dispatchers.Default. You can customize this by providing your own CoroutineContext. In the above example, you would probably want to do this work on Dispatchers.IO, as follows:

class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

override val coroutineContext = Dispatchers.IO

override suspend fun doWork(): Result = coroutineScope {
val jobs = (0 until 100).map {
async {
downloadSynchronously("https://www.google.com")
}
}

// awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
jobs.awaitAll()
Result.success()
}
}

CoroutineWorkers handle stoppages automatically by canceling the coroutine and propagating the cancellation signals. You don't need to do anything special to handle work stoppages.

2.3 Threading in RxWorker

We provide interoperability between WorkManager and RxJava2. To get started, include work-rxjava2 the dependency in addition to work-runtime in your gradle file. Then, instead of extending Worker, you should extend RxWorker. Finally override the RxWorker.createWork() method to return a Single<Result> indicating the Result of your execution, as follows:

public class RxDownloadWorker extends RxWorker {

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

@Override
public Single<Result> createWork() {
return Observable.range(0, 100)
.flatMap { download("https://www.google.com") }
.toList()
.map { Result.success() };
}
}

Note that RxWorker.createWork() is called on the main thread, but the return value is subscribed to a background thread by default. You can override RxWorker.getBackgroundScheduler() to change the subscribing thread.

Stopping an RxWorker will dispose of the Observers properly, so you don't need to handle work stoppages in any special way.

2.4 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 canceled 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;
});
}
}

You can read more about them here:

https://developer.android.com/reference/androidx/work/Worker https://developer.android.com/reference/androidx/work/ListenableWorker

https://developer.android.com/topic/libraries/architecture/workmanager/advanced/long-running

Thanks for reading…

--

--

Abhishek Srivastava

Senior Software Engineer | Android | Java | Kotlin | Xamarin Native Android | Flutter | Go