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 Creator named 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 .



val imageRepository: ImageRepository) :

ListenableWorker(context, workerParams) {



...



class Creator



override fun create(appContext: Context, params: WorkerParameters): ListenableWorker {

return WallpaperWorker(appContext, params, redditRepository)

}

}

} 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.

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

@Module

interface WorkerBindingModule {

@Binds

@IntoMap



fun bindWallpaperWorker(creator: WallpaperWorker.Creator): WorkerCreator

} interface WorkerBindingModule { @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 .



private val workerFactories: Map<Class<out ListenableWorker>,

) : 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)

}

} class CustomWorkerFactory @Inject constructor(private val workerFactories: Map , @JvmSuppressWildcards Provider >) : WorkerFactory() {override funappContext:workerClassName:workerParameters:): ListenableWorker? {val foundEntry =workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }val factoryProvider = foundEntry?.value?: throw IllegalArgumentException("unknown worker class name: $workerClassName")

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







override fun onCreate() {

super.onCreate()

WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(factory).build())

} class CustomApplication : DaggerApplication() { @Inject lateinit var workerFactory: WorkerFactoryoverride 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