Use of WorkManager in Android App (Part-4)
Welcome to the fourth post of our WorkManager series.
In this blog post, I’ll cover:
- Custom Work Manager configuration with Types of Worker classes
- 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 theWorkManager.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:
In most cases, the default initialization is all you need.
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 indoWork()
and return aResult
, your callback may not operate properly. If you find yourself in this situation, consider using aListenableWorker
(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. UnlikeWorker
, this code does not run on theExecutor
specified in yourConfiguration
. Instead, it defaults toDispatchers.Default
. You can customize this by providing your ownCoroutineContext
. In the above example, you would probably want to do this work onDispatchers.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()
}}
CoroutineWorker
s 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 Observer
s 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 ListenableFuture
s one of two ways:
- If you use Guava, use
ListeningExecutorService
. - Otherwise, include
councurrent-futures
in your gradle file and useCallbackToFutureAdapter
.
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…