Your ViewModelFactory is Boilerplate and it should be replaced
The ViewModel creation API evolved three times while we kept copy-pasting the same Factory class.
Your ViewModelFactory is Boilerplate and it should be replaced

It’s time to ditch the boilerplate
The ViewModel creation API evolved three times while we kept copy-pasting the same Factory class.
Every Android project has at least one file called SomethingViewModelFactory. It takes constructor parameters, overrides create(), casts the class type, and exists solely because ViewModels didn’t support constructor injection out of the box. We’ve been writing this boilerplate since 2017. It’s 2026 and most of us are still doing it.
The thing is, Google replaced ViewModelFactory twice already. First with CreationExtras in Lifecycle 2.5, then Compose made it irrelevant with viewModel(), and DI frameworks like Koin and Hilt eliminated it entirely from a different angle. Let’s look at what actually replaced it and which approach fits your codebase.
The Old Way (And Why It Won’t Die)
This is what most tutorials still teach:
// UserProfileViewModelFactory.kt
class UserProfileViewModelFactory(
private val userId: String,
private val userRepository: UserRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserProfileViewModel::class.java)) {
return UserProfileViewModel(userId, userRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// In your Fragment
val viewModel: UserProfileViewModel by viewModels {
UserProfileViewModelFactory(args.userId, userRepository)
}
One factory per ViewModel. The cast is unchecked. The error message is useless. And you’re maintaining a file that does nothing except call a constructor. If you have 30 ViewModels, you have 30 of these.
CreationExtras: Fixing the framework
Lifecycle 2.5 introduced CreationExtras, a type-safe key-value map that passes arguments to ViewModel creation without a custom Factory:
// UserProfileViewModel.kt
class UserProfileViewModel(
private val userId: String,
private val userRepository: UserRepository
) : ViewModel() {
companion object {
val USER_ID_KEY = object : CreationExtras.Key<String> {}
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val userId = extras[USER_ID_KEY] ?: error("userId required")
val app = extras[APPLICATION_KEY] as MyApp
val userRepository = app.appContainer.userRepository
return UserProfileViewModel(userId, userRepository) as T
}
}
}
}
// In your Fragment
val viewModel: UserProfileViewModel by viewModels(
extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(UserProfileViewModel.USER_ID_KEY, args.userId)
}
},
factoryProducer = { UserProfileViewModel.Factory }
)
Honestly, this is more code, not less. CreationExtras solves the type-safety problem but doesn’t solve the boilerplate problem. It’s useful if you’re building a framework or library that needs to pass data to ViewModels generically. For app code, there are better options.
Pro tip: CreationExtras gives you APPLICATION_KEY, SAVED_STATE_REGISTRY_OWNER_KEY, and VIEW_MODEL_STORE_OWNER_KEY for free. If your ViewModel only needs the Application context, you can use AndroidViewModel or pull it from extras without a custom factory.
Compose’s viewModel(): The Clean Path
If you’re in Compose, viewModel() with a factory lambda is the simplest approach:
// In your Composable
@Composable
fun UserProfileScreen(
userId: String,
userRepository: UserRepository = koinInject()
) {
val viewModel: UserProfileViewModel = viewModel {
UserProfileViewModel(userId, userRepository)
}
}
That’s it. No Factory class. No CreationExtras keys. The lambda is your factory. The ViewModel is still scoped to the navigation graph entry or the ViewModelStoreOwner, exactly like before.
This works because Compose’s viewModel() accepts an initializer lambda since Lifecycle 2.5. The ViewModel is created once and survives recomposition and configuration changes like it always has.
Koin: Zero Factories, Full DI
If you’re using Koin, ViewModelFactory never existed in your world:
// di/ViewModelModule.kt
val viewModelModule = module {
viewModelOf(::UserProfileViewModel)
}
// In your Composable
@Composable
fun UserProfileScreen(userId: String) {
val viewModel: UserProfileViewModel = koinViewModel(
parameters = { parametersOf(userId) }
)
}
Koin handles construction, parameter injection, and ViewModel scoping. The parametersOf() call passes runtime arguments like navigation IDs. No factory, no extras, no ceremony.
Warning: With Koin, make sure your ViewModel module is loaded before the screen that uses it. Lazy module loading with loadKoinModules() in a navigation graph works, but forgetting to unload them on pop causes memory leaks in modularized apps.
Hilt: Annotation-Driven, Zero Boilerplate
Hilt eliminates the factory through @HiltViewModel:
// UserProfileViewModel.kt
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = savedStateHandle["userId"] ?: error("userId required")
}
// In your Composable
@Composable
fun UserProfileScreen() {
val viewModel: UserProfileViewModel = hiltViewModel()
}
Runtime arguments come through SavedStateHandle, which Hilt populates from navigation arguments automatically. No factory. No parameters call. The DI container handles everything.
Which One Should You Use?
The answer depends on what’s already in your project:
- Pure Compose, no DI framework: Use viewModel { } with the initializer lambda. Delete your Factory files.
- Koin: Use koinViewModel(). You probably already are.
- Hilt: Use @HiltViewModel + hiltViewModel(). Pass runtime args via SavedStateHandle.
- Legacy View system, no DI: CreationExtras is your best incremental improvement. Verbose but eliminates per-ViewModel factory files.
The one thing you should not do is keep writing ViewModelProvider.Factory implementations in 2026. Every approach above is less code, more type-safe, and already supported by the libraries in your build.gradle.
Key Takeaways
- ViewModelProvider.Factory was a workaround for the lack of constructor injection in the ViewModel API. Every modern approach eliminates it.
- Compose’s viewModel { } initializer lambda is the simplest replacement if you don’t use a DI framework.
- Koin’s koinViewModel() and Hilt’s @HiltViewModel both handle ViewModel creation entirely through DI, no factory needed.
- CreationExtras is the framework-level solution, useful for library authors but verbose for app code.
- If you still have ViewModelFactory files in your project, delete them one by one as you touch those screens. There’s no migration, just a cleaner API waiting to be used.
Every Android project has at least one factory file that outlived its purpose. Now you know what replaced it.
메타데이터
- post_id
- bf75d957d0b4
- slug
- your-viewmodelfactory-is-boilerplate-and-it-should-be-replaced-bf75d957d0b4
- url
- https://medium.com/@androidblacksmith/your-viewmodelfactory-is-boilerplate-and-it-should-be-replaced-bf75d957d0b4
- canonical_url
- https://medium.com/@androidblacksmith/your-viewmodelfactory-is-boilerplate-and-it-should-be-replaced-bf75d957d0b4
- author_url
- https://medium.com/@androidblacksmith
- status
- ok
- fetched_at
- 2026-06-18 00:10:23