Illustration by Virginia Poltrack

Customizing WorkManager — fundamentals

Pietro Maggi
Android Developers
Published in
5 min readApr 6, 2020

--

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:

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:

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 right ListenableWorker class. This can fail if our workers class names are minimized by R8 (or ProGuard). To avoid that, WorkManager includes a proguard-rules.pro file that avoids obfuscation of your Worker 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 a ClassNotFoundException.

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:

  1. Disable the default initialization
  2. Implement a custom WorkerFactory
  3. Create a custom configuration
  4. 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”.

--

--