
Have you ever found yourself in this situation? You’re building an Android app with Hilt, everything’s going smoothly with dependency injection, and then suddenly you need to pass a user ID from your navigation arguments into a repository. How do you inject the repository when it needs runtime data that Hilt doesn’t know about yet?
This is where assisted injection comes into play.
The Problem: When Standard DI Falls Short
Let’s start with a scenario we’ve all encountered. You’re building a user profile screen. Your UserProfileViewModel needs a UserRepository to fetch user data. Simple enough—Hilt can inject that for you.
But here’s the catch: the repository needs to know which user to fetch. The user ID comes from your navigation arguments, determined at runtime when the user taps on a profile. You can’t know this ID when Hilt sets up your dependency graph at compile time.
So what do you do?
You could:
- Pass the user ID through multiple layers of your app manually
- Make the repository accept the ID in every method call
- Give up on dependency injection for this class
Or you could use assisted injection.
What is Assisted Injection?
Assisted injection is Hilt’s elegant solution for combining framework-injected dependencies with runtime parameters. You get the benefits of dependency injection while still passing dynamic data.
Think of it this way: Hilt assists you by injecting what it knows (like your database, API clients, etc.), while you provide what only you know at runtime (like user IDs, search queries, or configuration data).
The Three Annotations You Need to Know
@AssistedInject — Place this on your constructor instead of the regular @Inject. It signals to Hilt that this class needs a mix of injected and runtime parameters.
@Assisted — Mark runtime parameters with this annotation. These are the values you’ll provide when creating instances.
@AssistedFactory — Define a factory interface that Hilt implements. This factory is what you actually inject, and it provides a clean way to create instances with your runtime data.
The Golden Rule
You cannot inject @AssistedInject types directly.
Instead, you inject the factory and use it to create instances. This might seem like an extra step, but it’s actually the key to the whole pattern — the factory is the bridge between Hilt’s world and your runtime world.
Your First Assisted Injection: A Simple Repository
Let’s build something practical. We’ll create a UserRepository that needs both a database (from Hilt) and a user ID (from runtime).
Step 1: Create the Repository with @AssistedInject
class UserRepository @AssistedInject constructor(
private val database: AppDatabase, // Hilt provides this
@Assisted private val userId: String // You provide this
) {
fun getUserData(): UserData {
return database.userDao().getUser(userId)
}
fun updateUser(userData: UserData) {
database.userDao().update(userData)
}
}
Notice how clean this is? The database dependency comes from Hilt’s graph, while the userId is marked with @Assisted to indicate it’s runtime data.
Step 2: Define the Factory
@AssistedFactory
interface UserRepositoryFactory {
fun create(userId: String): UserRepository
}
The factory is simple: one method that takes your @Assisted parameters (in the same order as the constructor) and returns an instance of your class.
Step 3: Use the Factory in Your ViewModel
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepositoryFactory: UserRepositoryFactory,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = savedStateHandle["userId"] ?: ""
private val repository: UserRepository = userRepositoryFactory.create(userId)
fun loadUserProfile() {
val userData = repository.getUserData()
// Use the data...
}
}
See what happened here? We inject the factory, not the repository. Then we use the factory to create a repository instance with our runtime userId. Hilt handles injecting the database automatically when the factory creates the instance.
Assisted Injection with ViewModels
Starting with Hilt 2.42, you can use assisted injection directly in your ViewModels. This is perfect for when your ViewModel needs navigation arguments or other runtime data.
The Modern Way: Direct ViewModel Assisted Injection
@HiltViewModel(assistedFactory = ArticleViewModelFactory::class)
class ArticleViewModel @AssistedInject constructor(
private val articleRepository: ArticleRepository, // Injected
private val analyticsTracker: AnalyticsTracker, // Injected
@Assisted private val articleId: String, // Runtime
private val savedStateHandle: SavedStateHandle // Auto-provided by Hilt
) : ViewModel() {
private val _article = MutableStateFlow<Article?>(null)
val article: StateFlow<Article?> = _article.asStateFlow()
init {
loadArticle()
analyticsTracker.trackScreenView("article_$articleId")
}
private fun loadArticle() {
viewModelScope.launch {
_article.value = articleRepository.getArticle(articleId)
}
}
}
Don’t Forget the Factory
@AssistedFactory
interface ArticleViewModelFactory {
fun create(articleId: String): ArticleViewModel
}
Using It in Your Activity
@AndroidEntryPoint
class ArticleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val articleId = intent.getStringExtra("article_id") ?: ""
val viewModel: ArticleViewModel by viewModels(
extrasProducer = {
defaultViewModelCreationExtras
.withCreationCallback<ArticleViewModelFactory> { factory ->
factory.create(articleId)
}
}
)
setContent {
ArticleScreen(viewModel)
}
}
}
Your ViewModel gets both the benefits of Hilt’s dependency injection AND access to runtime data, such as navigation arguments.
Assisted Injection with Workers
WorkManager is another perfect use case. Workers need system-provided parameters (Context and WorkerParameters) plus your injected dependencies.
The @HiltWorker Annotation
@HiltWorker
class DataSyncWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted private val params: WorkerParameters,
private val syncRepository: SyncRepository, // Injected
private val notificationHelper: NotificationHelper // Injected
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val dataToSync = params.inputData.getString(KEY_SYNC_DATA) ?: ""
syncRepository.syncData(dataToSync)
notificationHelper.showSyncCompleteNotification()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
companion object {
const val KEY_SYNC_DATA = "sync_data"
}
}
Set Up Hilt’s WorkerFactory
In your Application class:
@HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
}
Enqueue the Worker
val workRequest = OneTimeWorkRequestBuilder<DataSyncWorker>() .setInputData( workDataOf(DataSyncWorker.KEY_SYNC_DATA to "user_data") ) .build() WorkManager.getInstance(context).enqueue(workRequest)
No factory needed for Workers! Hilt handles the factory creation automatically. Just annotate with @HiltWorker and mark Context and WorkerParameters with @Assisted.
Handling Multiple Parameters of the Same Type
What if you need multiple String parameters?
// This won't work! class MessageComposer @AssistedInject constructor( @Assisted private val senderId: String, @Assisted private val recipientId: String, // Ambiguous! @Assisted private val messageType: String // Also ambiguous! )
Hilt won’t know which String is which. The solution? Use identifiers.
Add String Identifiers to Each Parameter
class MessageComposer @AssistedInject constructor(
private val messagingService: MessagingService, // Injected
@Assisted("senderId") private val senderId: String,
@Assisted("recipientId") private val recipientId: String,
@Assisted("messageType") private val messageType: String
) {
fun sendMessage(content: String) {
messagingService.send(
from = senderId,
to = recipientId,
type = messageType,
content = content
)
}
}
Mirror the Identifiers in the Factory
@AssistedFactory
interface MessageComposerFactory {
fun create(
@Assisted("senderId") senderId: String,
@Assisted("recipientId") recipientId: String,
@Assisted("messageType") messageType: String
): MessageComposer
}
The identifiers must match exactly between the constructor and factory. This tells Hilt how to map the parameters correctly.
Best Practices: Do’s and Don’ts
DO: Use Assisted Injection for Runtime Data
Perfect use cases:
- Navigation arguments (user IDs, article IDs)
- Search queries or filters
- System-provided parameters (Context in Workers)
- Dynamic configuration values
DO: Keep Factories Simple
One factory per class. One method per factory
// Good
@AssistedFactory
interface UserRepositoryFactory {
fun create(userId: String): UserRepository
}
// Overcomplicated - avoid
@AssistedFactory
interface RepositoryFactory {
fun createUserRepository(userId: String): UserRepository
fun createPostRepository(postId: String): PostRepository
fun createCommentRepository(commentId: String): CommentRepository
}
DO: Use Descriptive Identifiers
When you have multiple parameters of the same type:
// Clear and descriptive
@Assisted("userId") userId: String
@Assisted("sessionId") sessionId: String
// Cryptic - harder to maintain
@Assisted("id1") userId: String
@Assisted("id2") sessionId: String
DON’T: Try to Inject @AssistedInject Types Directly
// This will fail! @Inject lateinit var repository: UserRepository // Error // Do this instead @Inject lateinit var repositoryFactory: UserRepositoryFactory // Good val repository = repositoryFactory.create(userId)
DON’T: Use Assisted Injection for Static Dependencies
// Bad - database can be injected normally @AssistedInject constructor( @Assisted database: AppDatabase // Don't do this ) // Good - only runtime data is assisted @AssistedInject constructor( database: AppDatabase, // Injected by Hilt @Assisted userId: String // Runtime parameter )
Job Offers
Conclusion
Assisted injection is one of those features that seems complex at first but becomes second nature once you understand the pattern. It elegantly solves a real problem: how to use dependency injection when you need runtime data.
Key takeaways:
- Assisted injection combines compile-time DI with runtime parameters
- You inject the factory, not the class itself
- Use it for ViewModels with navigation arguments, Workers with system parameters, and any class that needs runtime data.
- Keep factories simple: one method, matching parameter order
- Use identifiers when you have multiple parameters of the same type
This article was previously published on proandroiddev.com



