Skip to content

How to Use @HiltWorker with AssistedInject in Android WorkManager

The Problem

I was setting up background sync jobs in an Android app when I hit a wall. My Worker needed both runtime parameters (Context, WorkerParameters) and compile-time dependencies (Repository, AnalyticsService). Standard Hilt @Inject wouldn’t work.

Build Error
error: [Dagger/MissingBinding] android.content.Context cannot be provided
without an @Provides-annotated method.

The issue is fundamental: Workers are created by WorkManager at runtime, not by Hilt at compile time. Hilt can’t inject into a class it doesn’t instantiate.

Why Standard @Inject Fails

When I tried this:

WRONG: Standard @Inject doesn't work
@HiltWorker
class SyncWorker @Inject constructor(
private val context: Context, // Runtime parameter
private val workerParams: WorkerParameters, // Runtime parameter
private val repository: DataRepository // Hilt dependency
) : CoroutineWorker(context, workerParams) {
// ...
}

The build failed because:

  1. Context and WorkerParameters are runtime values - WorkManager creates them when enqueueing work
  2. Hilt can’t provide runtime parameters - It only knows about compile-time dependencies
  3. @HiltWorker alone isn’t enough - It tells Hilt to generate a factory, but the factory still needs to receive runtime values

This is the core tension: dependency injection expects all dependencies at compile time, but Workers require runtime parameters.

The Solution: @AssistedInject

The correct pattern uses @AssistedInject for the constructor and @Assisted for runtime parameters:

CORRECT: @HiltWorker with @AssistedInject
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val workerParams: WorkerParameters,
private val repository: DataRepository, // Hilt-injected
private val analytics: AnalyticsService // Hilt-injected
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val data = repository.fetchData()
analytics.trackSync()
return Result.success()
}
}

Here’s what each annotation does:

AnnotationPurposeApplied To
@HiltWorkerTells Hilt to generate a factory for this WorkerClass
@AssistedInjectMarks constructor for assisted injectionConstructor
@AssistedMarks parameters provided at runtimeContext, WorkerParameters
No annotationHilt provides these dependenciesRepository, Analytics

The distinction is crucial: @Assisted parameters come from WorkManager at runtime, while unannotated parameters come from Hilt at compile time.

Setting Up HiltWorkerFactory

The Worker class alone isn’t enough. I also needed to configure the Application to use Hilt’s WorkerFactory.

Step 1: Update Application Class

MyApplication.kt
@HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}

Step 2: Disable Default WorkManager Initializer

In AndroidManifest.xml, remove the default initializer:

AndroidManifest.xml
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- Disable default WorkManager initializer -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
tools:node="remove" />
</provider>

This step is critical. Without it, WorkManager uses its default factory instead of Hilt’s factory, and your @AssistedInject constructor won’t be invoked.

Enqueueing Work

Once configured, enqueuing work works the same as always:

Enqueueing a Hilt Worker
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(workRequest)

WorkManager creates the Worker using Hilt’s factory, which:

  1. Receives Context and WorkerParameters from WorkManager
  2. Gets Repository and AnalyticsService from Hilt’s dependency graph
  3. Calls your @AssistedInject constructor with all parameters

Common Mistakes

I made these mistakes when first implementing this pattern.

Mistake 1: Using @Inject Instead of @AssistedInject

WRONG
@HiltWorker
class SyncWorker @Inject constructor( // Should be @AssistedInject
@Assisted private val context: Context,
// ...
)

Result: Build fails with “Dagger does not support injection into private constructors” or similar errors.

Mistake 2: Forgetting @Assisted on Runtime Parameters

WRONG
@HiltWorker
class SyncWorker @AssistedInject constructor(
private val context: Context, // Missing @Assisted
private val workerParams: WorkerParameters, // Missing @Assisted
private val repository: DataRepository
)

Result: Hilt tries to provide Context and WorkerParameters from the dependency graph, which fails.

Mistake 3: Not Disabling Default Initializer

If you forget to remove the default WorkManagerInitializer, the app will crash or silently use the wrong factory.

Mistake 4: Not Implementing Configuration.Provider

WRONG: Missing Configuration.Provider
@HiltAndroidApp
class MyApplication : Application() { // Should implement Configuration.Provider
@Inject
lateinit var workerFactory: HiltWorkerFactory
// Missing workManagerConfiguration override
}

Result: WorkManager uses default factory, Hilt injection doesn’t happen.

@Inject vs @AssistedInject: When to Use Which

The pattern is clear once you understand the distinction:

Inject vs AssistedInject decision
flowchart TD
A[Need to inject dependencies?] --> B{Who creates the instance?}
B -->|Hilt creates it| C[Use @Inject]
B -->|Framework creates it| D{Need runtime params?}
D -->|No| E[Use @Inject in factory]
D -->|Yes| F[Use @AssistedInject]
C --> G[Example: ViewModel, Repository]
E --> H[Example: Fragment with FragmentFactory]
F --> I[Example: Worker, Activity]

Use @Inject when:

  • Hilt controls the lifecycle (ViewModels, Repositories, Services)
  • All dependencies are known at compile time

Use @AssistedInject when:

  • A framework creates the instance (Workers, Activities)
  • You need runtime parameters mixed with compile-time dependencies

Passing Additional Runtime Data

Sometimes you need to pass custom data beyond Context and WorkerParameters. Use inputData:

Passing custom runtime data
// Enqueueing with input data
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(
workDataOf(
"userId" to userId,
"syncType" to "full"
)
)
.build()
// Reading in Worker
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val workerParams: WorkerParameters,
private val repository: DataRepository
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val userId = inputData.getString("userId")
val syncType = inputData.getString("syncType")
repository.syncUser(userId, syncType)
return Result.success()
}
}

This pattern keeps the constructor clean while still allowing runtime data to flow into the Worker.

The Architecture

Here’s how all the pieces connect:

HiltWorker sequence
sequenceDiagram
participant App as Application
participant WM as WorkManager
participant HF as HiltWorkerFactory
participant Hilt as Hilt Component
participant W as SyncWorker
Note over App: 1. App implements Configuration.Provider
App->>HF: Inject HiltWorkerFactory
Note over WM: 2. Enqueue work request
WM->>HF: createWorker(context, params, workerClass)
HF->>Hilt: Get compile-time dependencies
Hilt-->>HF: Repository, AnalyticsService
HF->>W: @AssistedInject(context, params, repo, analytics)
W-->>WM: Worker instance ready

The HiltWorkerFactory acts as a bridge between WorkManager’s runtime requirements and Hilt’s compile-time dependency graph.

Why This Matters

This pattern came up in a senior-level Android quiz that many experienced developers struggled with. One commenter noted that WorkManager dependency injection is “something most devs only discover during a production audit.”

The reason it trips people up is that it combines several concepts:

  1. Dependency injection fundamentals
  2. Android lifecycle management
  3. WorkManager’s factory pattern
  4. Hilt’s code generation

Each concept is understandable alone, but combining them requires understanding how all the pieces interact.

Summary

The @HiltWorker + @AssistedInject pattern solves a specific problem: injecting compile-time dependencies into classes that require runtime parameters.

Key points:

  • Workers are created by WorkManager, not Hilt, so standard @Inject doesn’t work
  • @AssistedInject lets you mix runtime parameters (@Assisted) with Hilt dependencies
  • You must configure HiltWorkerFactory in your Application class
  • Disable the default WorkManager initializer in the manifest
  • Use inputData for additional runtime values beyond Context and WorkerParameters

This pattern is essential for production-grade background work in Android. Without it, you’re stuck choosing between no dependency injection or manual dependency passing, both of which create maintenance problems as your app grows.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments