WorkManager with Dagger2

Dilpreet Singh
4 min readOct 15, 2019

--

Photo by Patrick Perkins

WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.

While scheduling some Worker you might require some external dependencies, and if you are using dagger then you want to inject these dependencies to the Worker but turns out it is not a trivial task, a worker is initialized by the WorkManager which already depends upon Context and WorkerParameters and dagger does not support constructor injection for WorkManager out of the box.

Let's see how to solve this problem.
We will create an app that randomly sets a wallpaper. I will only focus on stuff related to WorkManager and Dagger. This will also give a brief introduction to schedule Workers using WorkManager for those who are new to WorkManager. This post assumes that you have some experience working with Dagger2.

Our Simple WallpaperWorker

class WallpaperWorker(
context: Context,
workerParams: WorkerParameters
) : ListenableWorker(context, workerParams) {
override suspend fun doWork(): Result {
try {
setWallpaper()
} catch (e: Exception) {
return Result.failure()
}
return Result.success()
}

private fun setWallpaper() {
// Gets a image from assets folder and sets the wallpaper
}
}

We have a WallpaperWorker which when scheduled sets the wallpaper using our function setWallpaper() using the images present in the assets folder.

We can schedule a task by writing creating a periodic work request and enqueue it using the WorkManager.

val uploadWorkRequest =
PeriodicWorkRequest.Builder(WallpaperWorker::class.java, 1, TimeUnit.HOURS).build()
WorkManager.getInstance(context).enqueue(uploadWorkRequest)

Schedule image fetching from an API

Getting images from assets is pretty boring let’s fetch images from Lorem Picsum and set them as wallpaper.

For this purpose we have created a repository that helps fetches data from this API.

interface ImageRepository {
fun fetchImages()
}

This ImageRepository is added to our dagger dependency graph and will be accessible using dependency injection. We will now have to update our Worker implementation.

class WallpaperWorker(
context: Context,
workerParams: WorkerParameters,
val imageRepository: ImageRepository // Newly added dependency
)

We cannot perform constructor injection here as a Worker is created using a WorkerFactor, but we do have access to create our own Custom WorkerFactory. But how will this help solve the problem?

A factory object that creates ListenableWorker instances. The factory is invoked every time a work runs.

So, if we can handle the creation of our Worker then we could probably delegate the task of injecting the dependencies to a Factory or Creator.

The role of this Creatornamed WorkerCreator will be to actually create the Worker instance while the role of our CustomWorkerFactory will be to create an instance of WorkerCreator which in our case will be WallpaperWorkerCreator and provide Context and WorkerParameters to our WorkerCreator.

This interface defines the contract for a WorkerCreator.

interface WorkerCreator {
fun create(appContext: Context, params: WorkerParameters): ListenableWorker
}

We will create a nested class inside our WallpaperWorker.

class WallpaperWorker(context: Context, workerParams: WorkerParameters,
val imageRepository: ImageRepository) :
ListenableWorker(context, workerParams) {

...

class Creator @Inject constructor(val redditRepository: RedditRepository): WorkerCreator {

override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {
return WallpaperWorker(appContext, params, redditRepository)
}
}
}

The implementation of WorkerCreator is actually creating the WallpaperWorker and the create() function provides us with the context and params when called, while the redditRepository is injected in the constructor. We have this creator but how will this creator link to our CustomWorkerFactory and how are the dependencies injected in this creator instance? These are two problems let's solve them one by one.

Bind Creator instance to Worker class in a Map

We need to bind the WallpaperWorker class to the WallpaperWorker.Creator instance. For that first, we need to create an annotation that will work as the Map Key.

@MapKey
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)

Let’s bind the class name to the Creator instance.

@Module
interface WorkerBindingModule {
@Binds
@IntoMap
@WorkerKey(WallpaperWorker::class)
fun bindWallpaperWorker(creator: WallpaperWorker.Creator): WorkerCreator
}

On building, Dagger will automatically generate a Class which gets the dependencies from the dependency graph for the WallpaperWorker.Creator.

Now we are just left with linking this map with the WorkerFactory.

class CustomWorkerFactory @Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val foundEntry =
workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
val factoryProvider = foundEntry?.value
?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
return factoryProvider.get().create(appContext, workerParameters)
}
}

This WorkerFactory gets the Map from the dagger dependency graph and creates the WorkerCreator .

We are left with one last thing, we need to register our newly made WorkerFactory in our Application class and disable the default WorkManagerInitializer in AndroidManifest.xml

class CustomApplication : DaggerApplication() {
@Inject lateinit var workerFactory: WorkerFactory

override fun onCreate() {
super.onCreate()
WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(factory).build())
}
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<application android:name=".CustomApplication">
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove"/>
</application>
</manifest>

Conclusion

We learned how to perform constructor injection in a Worker using Dagger. One thing to keep in mind that although this approach helped us inject the dependencies. But for every worker type we create, we will have to write the same boilerplate code to set up the injection. We can reduce this manual effort but that will be for another article. Spoiler alert: AssistedInject

If you have any questions, thoughts or suggestions then I’d love to hear from you!
You can find me on Twitter, LinkedIn, and Github.

--

--