WorkManager is ubiquitous in modern android development. We use it to run any background work which should not be bound to the application process lifecycle. It was first introduced in May 2018 and replaced several scheduling frameworks. The library unifies it all with a concise and simple API. You no longer have to orchestrate JobScheduler, GcmNetworkManager, Evernote AndroidJob, and so on. In fact, this new API is saving us quite a lot of maintenance work, but is it perfect? There are a few rough areas we’ll explore in this article. Dependency injection It’s hard to imagine a modern production-grade android application without dependency injection. In our example, we’ll use Dagger2, but the principle will remain the same for any DI framework. Consider that you have a job that runs every hour. Its purpose is to delete old telemetry records from an SQLite database. The naive approach would be to create a component inside the worker and call : inject class CleanTelemetryWorker(context: Context, params: WorkerParameters) : Worker(context, params) { @Inject lateinit var telemetryRepository: TelemetryRepostory init { CleanTelemetryWorkerComponent.Factory.create(context).inject() } override fun doWork(): ListenableWorker.Result { telemetryRepository.clean() } } This solution is not ideal for a few reasons: The dependency inversion principle breaks since the component should not know how to get its dependencies This worker is hard to test because there’s no easy way to mock dependencies, but we’ll get to it later What we want is to be able to declare any worker like this: class CleanTelemetryWorker @Inject constructor( context: Context, params: WorkerParams, telemetryRepository: TelemetryRepository, ...//any other external dependencies ) Can we do better? We can! As you may know, we can customize WorkManager using the class: Configuration class MyApplication() : Application(), Configuration.Provider { override fun getWorkManagerConfiguration() = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .setWorkerFactory(myCustomWorkerFactory) .build() } You override in your class and provide a config object. The most important part here is the method. It allows us to pass a component that will create jobs for us. It sounds like the ideal place to inject our dependencies into workers. getWorkManagerConfiguration Application setWorkerFactory has only one method we can override, and it’s called… : WorkerFactory createWorker class CustomWorkerProvider { override createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? { //We need to provide workers here. } } Now we can provide arguments into our worker’s constructor. Fortunately, we don't have to do it ourselves. There exists this handy tool to generate dependency provision for us called Dagger 2. Let’s try to create a simple and robust schema to inject dependencies into workers. Of course there’s plenty of ways to do this, but let’s start simple and then add some options to it as we go. So what exactly do we need to create a worker? The class has two required dependencies: and . Let’s start with the dagger component that we will init in the function. ListenableWorker Context WorkerParameters WorkerComponent createWorker @Component interface WorkerComponent { @Component.Factory interface Factory { fun create( @BindsInstance context: Context, @BindsInstance workerParameters: WorkerParameters, ): WorkerComponent } } We use annotation to tell dagger to create an implementation of which will return us the implementation. Component.Factory WorkerComponent.Factory WorkerComponent annotation in the factory’s method means that this component should be able to provision and using the objects we passed to the method. @BindsInstance create Context WorkParameters Cool, we can now provide workers by adding a method that returns a concrete worker class, like CleanTelemetryWorker @Component(dependencies=[ApplicationComponent::class]) interface WorkerComponent { @Component.Factory interface Factory { fun create( applicationComponent: ApplicationComponent, @BindsInstance context: Context, @BindsInstance workerParameters: WorkerParameters, ): WorkerComponent } fun cleanTelemetryWorker(): CleanTelemetryWorker ... } Notice that we need to add dependencies to the component’s annotation because by itself can’t provide you with anything other than and . We also have to pass the dependency component as a factory argument. WorkerComponent Context WorkerParameters Let’s see how the will look like now: CustomWorkerProvider class CustomWorkerProvider(private val applicationComponent: ApplicationComponent) { override createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? { val component = DaggerWorkerComponent.factory().create(applicationComponent, appContext, workerParameters) when(workeClassName) { ClientTelemetryWorker::class.qualifiedName -> component.cleanTelemetryWorker() ... } } } It looks much better, but every time we add a new worker, we have to provide it here and add a new function definition to the component. Can dagger2 help us with this? Dagger Multibinding! With dagger multi binding, we can declare a module for each worker which will provide a worker provider in a map, where keys are the java classes of the workers. @Module interface CleanTelemetryWorkerModule { @Binds @IntoMap @ClassKey(CleanTelemetryWorker::class.java) fun bindCleanTelemetryWorker(cleanTelemetryWorkerProvider: Provider<CleanTelemetryWorker>): Provider<out ListenableWorker> } Let’s add this module to the component: @Component(dependencies=[ApplicationComponent::class], modules=[CleanTelemetryWorkerModule::class],) interface WorkerComponent { ... } And finally, our will look like this: CustomWorkerProvider class CustomWorkerProvider(private val applicationComponent: ApplicationComponent) { override createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? { val component = DaggerWorkerComponent.factory().create(applicationComponent, appContext, workerParameters) val workerProvidersMap = component.workerProvidersMap() return workerProvidersMap[Class.forName(workerClassName)]!!.get() } } Every time you have to add a new job, you’ll only have to create a new module and attach it to the . Neat, right? WorkerComponent Why does it matter, after all? The jobs now look like any other class with externally declared dependencies. It allows us to enforce the dependency inversion principle of SOLID. With the relatively small overhead, we achieve the following benefits: Testability We can mock job dependencies or pass different implementations at any time. Jobs are not aware of the dependency inversion framework. If you choose to switch from Dagger 2 to any other DI framework, your jobs will remain the same. The only thing that will change is the external glue. Ease of support Reducing mental stress on the developers is also a thing. Previously every time you created a job, you had to think about how to get your dependencies there. It’s not always trivial, especially if your dependency graph has different scopes. In the following article, we’ll reap the benefits of this setup and explore how to properly test your workers now that we have an easy way to mock the dependencies. I hope this article was helpful! Let me know what you think :)