Customizing WorkManager — fundamentals
Welcome to the fifth post in our WorkManager series. WorkManager is an Android Jetpack library that runs deferrable, guaranteed background work when the work’s constraints are satisfied. It is the current best practice for this kind of work on Android.
If you’ve been following thus far, we’ve talked about:
- What WorkManager is and when to use WorkManager
- How to use the WorkManager API to schedule work
- WorkManager and Kotlin
- WorkManager Periodicity
In this article, we’re going to talk about custom configuration, covering:
- Why we may need a custom configuration
- How to define a custom configuration
- What is a
WorkerFactory
and why we need a custom one - What is the
DelegatingWorkerFactory
There’s a sixth blog post that expands these concepts to Dependency Injection and Dagger in particular that covers:
- Use Dagger to inject parameters in our
WorkerFactory
- On-Demand initialization
The two articles are connected and the second one requires knowledge presented in this one.
Stating the problem
When using WorkManager, it’s your responsibility to define Worker
/CoroutineWorker
or any other ListenableWorker
derived classes. WorkManager instantiates your workers, at the right time, independently from having your application running in the foreground or running at all. To instantiate your worker class WorkManager uses a WorkerFactory
.
Workers created by the default WorkerFactory
have access to only 2 parameters:
- Application’s
Context
WorkerParameters
If you need to have additional parameters passed to your worker’s constructor, you need a custom WorkerFactory
.
For the curious: We said that the default
WorkerFactory
uses reflection to instantiate the rightListenableWorker
class. This can fail if our workers class names are minimized by R8 (or ProGuard). To avoid that, WorkManager includes aproguard-rules.pro
file that avoids obfuscation of yourWorker
class names.
Custom configuration and WorkerFactory
The WorkManager class follows the singleton pattern and it can only be configured before instantiation. This means that if you want a custom configuration, you need to disable the default configuration first.
If you try and initialize WorkManager a second time using the initialize()
method an exception (added in v1.0.0) will be thrown. To avoid this, disable the default initialization. You can then configure and initialize WorkManager in your Application’s onCreate
method.
A newer, better way to initialize WorkManager has been added in v2.1.0. You can use an on-demand initialization by implementing WorkManager’s Configuration.Provider
interface in your Application
class. Then you just need to get the instance using getInstance(context)
and WorkManager will initialize WorkManager using your custom configuration.
Configurable parameters
As we already said, you can configure the WorkerFactory
used to create the workers, but you can also customize other parameters. The full list of parameters is in WorkManager’s reference guide for the Configuration.Builder
. Here I want to call out two additional parameters:
- Logging level
JobId
range
Modifying the logging level comes in handy when we need to understand what is going on with WorkManager. We have a documentation page on this topic. You can take a look at the Advanced WorkManager codelab to see how this is implemented in a real sample and what kind of information you can obtain.
We may want to customize the JobId
range, if we are using WorkManager as well as the JobScheduler API in our application. In this case you want to avoid using the same JobId
range in both places. There’s also a new Lint rule that covers this case introduced in v2.4.0.
WorkManager’s WorkerFactory
We already know WorkManager has a default WorkerFactory
that uses reflection to find which class to instantiate based on the Worker
class name we passed in our WorkRequest
.
⚠️ If you create a
WorkRequest
and then you refactor the app using a different class name for your worker, WorkManager will not be able to find the right class and will throw aClassNotFoundException
.
You probably want to add other parameters to your worker’s constructor. Imagine having a worker that expects a reference to a Retrofit service needed to communicate with a remote server:
If we make this change to an application it will still compile, but, as soon as we execute it and WorkManager tries to instantiate this CoroutineWorker
class, the application will be closed with an exception, complaining that it’s not possible to find the right init method to instantiate:
Caused by java.lang.NoSuchMethodException: <init> [class android.content.Context, class androidx.work.WorkerParameters]
We need a custom WorkerFactory
!
But, not so fast: we already saw that there are few steps involved. Let’s recap what we have to do, then dive into each item details:
- Disable the default initialization
- Implement a custom
WorkerFactory
- Create a custom configuration
- Initialize WorkManager
Disable the default initialization
As described in WorkManager’s documentation, disabling has to be done in your AndroidManifest.xml
file, removing the node that is merged automatically from the WorkManager library by default.
Implement a custom WorkerFactory
We now need to write our own factory that creates our worker with the right parameters:
Create a custom WorkerConfiguration
Next, we have to register our factory in our WorkManager’s custom configuration:
Initialize WorkManager
This is all you need if you have a single Worker class type in your application. If you have more than one, or you expect to have more in the future, a better solution is to use the DelegatingWorkerFactory
introduced in v2.1.
DelegatingWorkerFactory
Instead of configuring WorkManager to directly use our factory, we can use a DelegatingWorkerFactory
and add to it our own WorkerFactory
using its addFactory()
method. You can then have multiple factories where each one takes care of one or more workers. Register your factory with the DelegatingWorkerFactory
and it will take care to coordinate the multiple factories.
In this case your factory needs to check if the workerClassName
passed as a parameter is something that it knows how to handle. If not, it returns null
and the DelegatingWorkerFactory
will move to the next registered factory. If none of the registered factories know how to handle a class, it will fall back to the default factory that uses reflection.
Here’s our factory modified to return null
if it doesn’t know how to handle a workerClassName
:
Our WorkManager configuration then becomes:
If you have more than one Worker
that requires different parameters, you can create a second factory and add it calling addFactory
a second time.
Conclusions
WorkManager is a powerful library, able to cover a lot of common use cases with its default configuration. However there are some cases when you need to increase its debugging level or need to pass additional parameters to your worker. In these cases you need a custom configuration.
I hope that this article has given you a good overview of this topic, let me know if you have any questions in the comments or contacting me directly on twitter at pfmaggi@.
The next article covers how to use Dagger in a WorkManager custom configuration: “Customizing WorkManager with Dagger”.