WorkManager with Hilt and App Startup 🚀

Santiago Mattiauda
4 min readFeb 1, 2024
Photo by Daniel Romero on Unsplash

Motivation

In recent times, the Android team has provided us with a large number of libraries that aim to improve different aspects that I consider essential in an Android application, such as startup and dependency injection. They provide us with an ecosystem where each solution builds upon the others, which is commonly known as “capabilities,” a term used for this type of functionality.

But in some cases, I have encountered some type of error, such as the following.

java.lang.NoSuchMethodException: com.example.MyWorkerWorker.<init> [class android.content.Context, class androidx.work.WorkerParameters]
at java.lang.Class.getConstructor0(Class.java:2332)
at java.lang.Class.getDeclaredConstructor(Class.java:2170)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:95)
at androidx.work.impl.WorkerWrapper.runWorker(WorkerWrapper.java:245)
at androidx.work.impl.WorkerWrapper.run(WorkerWrapper.java:137)
at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:929)

This error is well known by developers who use WorkManager and Hilt at the same time, but in my case I had one more variant App Startup.

In this article, we will see an alternative that was useful to me to solve this error in a more complex application than the one we will follow as an example, but that, in my opinion, maintains the benefits that Jetpack offers us.

Let’s start with the tutorial, so to speak, of WorkManager with Hilt. These are the steps you need to follow to use Hilt in your WorkManager configurations.

How to use WorkManager with Hilt

First, to use WorkManager support, we need to add the dependency that allows us to use Hilt functions in WorkManager. In this case, we are going to use KSP.

dependencies {
implementation("androidx.hilt:hilt-work:1.1.0")
// When using Kotlin.
ksp("androidx.hilt:hilt-compiler:1.1.0")
}

đź’ˇ Important: To take into account the version of Hilt that supports KSP.

Once the dependencies are added, we can use the annotation @HiltWorker in the class and @AssistedInject in the constructor of our Worker. You can only use @Singleton objects or unscoped bindings in Worker objects. Additionally, you must annotate the dependencies Context and WorkerParameters with @Assisted:

@HiltWorker
class MyWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val crashTrackerService: CrashTrackerService,
) : CoroutineWorker(appContext, workerParams) { ... }

Next, make your Application class implement the Configuration.Provider interface, inject an instance ofHiltWorkFactory, and pass it to the WorkManager configuration as follow:

@HiltAndroidApp
class MainApplication : Application(), Configuration.Provider {

@Inject
lateinit var workerFactory: HiltWorkerFactory

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

Due to this action customizing the WorkManager configuration, you must also remove the default initializer from the AndroidManifest.xml file, as specified in the official WorkManager documentation. This is because starting from version 2.6.0-alpha01 of WorkManager or later, it uses the initializer from androidx.startup. We will see later how we can continue to leverage the benefits of App Startup at this point.

Complete example up to this point:

Once WorkManager is configured through Hilt, let’s see how we can enhance our implementation by incorporating App Startup.

App Startup 🚀 + Hilt 🗡

To avoid repeating information and highlight Bartek Lipinski excellent article in his post, he explains how we can initialize our components provided by Hilt during the startup of our app.

💡 Read full article: App Startup 🚀 + Hilt 🗡

We are going to use its implementation where it defines:

class DependencyGraphInitializer : Initializer<Unit> {

override fun create(context: Context): Unit {
//this will lazily initialize ApplicationComponent before Application's `onCreate`
InitializerEntryPoint.resolve(context)
return Unit
}

override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}

}

How to initialize the Hilt dependency graph and, consequently, allow other initializers that need to use Hilt to have it as a dependency. Let’s see how this would be in our previous case for the custom configuration of WorkManager, but using a custom initializer.

WorkManager + App Startup + Hilt

For this purpose, we create our WorkManagerInitializer class implementing the Initializer interface, where we configure WorkManager as follows:

class WorkManagerInitializer : Initializer<WorkManager>, Configuration.Provider {

@Inject
lateinit var workerFactory: HiltWorkerFactory

override fun create(context: Context): WorkManager {
InitializerEntryPoint.resolve(context).inject(this)
WorkManager.initialize(context, workManagerConfiguration)
return WorkManager.getInstance(context)
}

override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(DependencyGraphInitializer::class.java)
}

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

Once the initializer is defined, similar to what we did in the Application class, we now implement the Configuration.Provider interface. We inject an instance of HiltWorkFactory and pass it to the WorkManager configuration, all within the create method of the Initializer interface. Now, we can remove that configuration from the Application class.

Next, we add our initializer to the AndroidManifest.

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">

<meta-data
android:name="com.santimattius.android.startup.initializer.DependencyGraphInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.santimattius.android.startup.initializer.CrashTrackerInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.santimattius.android.startup.initializer.WorkManagerInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />

</provider>

As we can see in the above code, we still keep the removal of androidx.work.WorkManagerInitializer. We leave this to remove the initializer that comes with the WorkManager library.

And there you have it — now we have the complete configuration to use WorkManager with Hilt and App Startup.

Complete example up to this point:

Conclusion.

While not every project may require these implementations, it’s worthwhile to embrace the individual benefits each solution brings, as we’ve seen throughout this article:

  • App Startup: providing a simple and effective way to initialize components when an application starts.
  • Hilt: reducing the repetitive work of manually injecting dependencies into your project and automatically managing their lifecycles.

References

--

--