ViewModel
This page covers how to declare a ViewModel in a Koin module, on every platform that supports AndroidX ViewModel (Android, iOS, desktop, web). It assumes you know definitions. To retrieve a ViewModel, see the platform pages: Android ViewModel for by viewModel() in an Activity or Fragment, and ViewModel in Compose for koinViewModel().
Dependencies
The declaration DSL is in koin-core-viewmodel, a multiplatform artifact:
// build.gradle.kts, commonMain
implementation("io.insert-koin:koin-core-viewmodel")
The injection APIs come with the platform artifacts: io.insert-koin:koin-android (it includes koin-core-viewmodel) and io.insert-koin:koin-compose-viewmodel. Versions come from the BOM: see Gradle Setup.
Declaring a ViewModel
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
import org.koin.dsl.module
import org.koin.plugin.module.dsl.viewModel
class UserViewModel(private val repository: UserRepository) : ViewModel()
val appModule = module {
viewModel<UserViewModel>()
}
import org.koin.core.annotation.KoinViewModel
@KoinViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel()
With the Compiler Plugin, @KoinViewModel is in org.koin.core.annotation and works in common code. The KSP processor used org.koin.android.annotation.KoinViewModel.
import org.koin.dsl.module
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.module.dsl.viewModel
val appModule = module {
viewModelOf(::UserViewModel)
// or with the constructor call written out
viewModel { UserViewModel(get()) }
}
A viewModel definition creates a new instance on each request, like a factory. The platform ViewModelStore (Activity, Fragment, navigation entry) keeps the instance, so the same owner gets the same ViewModel back, across configuration changes on Android. Do not declare a ViewModel as single: it would be shared by every screen and never cleared.
Passing parameters
A ViewModel often needs a value known only at runtime, such as an item id. Mark the constructor parameter as an injected parameter:
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
class DetailViewModel(
@InjectedParam val itemId: String,
private val repository: DetailRepository,
) : ViewModel()
val appModule = module {
viewModel<DetailViewModel>()
}
@KoinViewModel
class DetailViewModel(
@InjectedParam val itemId: String,
private val repository: DetailRepository,
) : ViewModel()
val appModule = module {
viewModel { params ->
DetailViewModel(itemId = params.get(), repository = get())
}
}
viewModelOf(::DetailViewModel) also works: it reads parameters by type from the values passed by the caller.
The caller passes the value with parametersOf, whatever the declaration style:
// Android
val viewModel: DetailViewModel by viewModel { parametersOf("item_123") }
// Compose
val viewModel = koinViewModel<DetailViewModel> { parametersOf("item_123") }
Parameters are read once, when the ViewModel is created. If the same owner asks again with another value, it gets the existing instance. More on parameters in Injected Parameters.
SavedStateHandle
Add a SavedStateHandle parameter to the constructor. Koin creates it from the ViewModel CreationExtras, with no declaration:
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
class ProfileViewModel(
private val handle: SavedStateHandle,
private val repository: UserRepository,
) : ViewModel() {
val userId: String? = handle["userId"]
}
val appModule = module {
viewModel<ProfileViewModel>()
}
@KoinViewModel
class ProfileViewModel(
private val handle: SavedStateHandle,
private val repository: UserRepository,
) : ViewModel() {
val userId: String? = handle["userId"]
}
val appModule = module {
viewModelOf(::ProfileViewModel)
// or: get() resolves the SavedStateHandle
viewModel { ProfileViewModel(get(), get()) }
}
On Android, navigation arguments and Activity extras are in the SavedStateHandle. Inject it in the constructor only. If the ViewModel owner has no SavedStateRegistryOwner, Koin fails with "Koin could not create a SavedStateHandle: the ViewModel's CreationExtras has no SavedStateRegistryOwner."
ViewModel scope
A ViewModel can own a scope: dependencies created with it and released when it is cleared. Declare them with the viewModelScope archetype:
- Compiler Plugin DSL
- Annotations
- Constructor / Classic DSL
val appModule = module {
viewModelScope {
scoped<UserCache>()
viewModel<UserViewModel>()
}
}
@ViewModelScope
class UserCache
@KoinViewModel
@ViewModelScope
class UserViewModel(private val cache: UserCache) : ViewModel()
val appModule = module {
viewModelScope {
scopedOf(::UserCache)
viewModelOf(::UserViewModel)
}
}
Enable the option at start, or the ViewModel gets no scope:
startKoin {
options(viewModelScopeFactory())
modules(appModule)
}
viewModelScope and viewModelScopeFactory() require opting in to @KoinViewModelScopeApi and @KoinExperimentalAPI. A ViewModel that takes scoped dependencies in its constructor must be declared inside viewModelScope { }. Each ViewModel instance gets its own UserCache, closed when the ViewModel is cleared.
Limits
- One instance per owner: injected parameters are ignored once the ViewModel exists for that owner. Use a different key or owner to get a second instance.
- No
singleViewModel: a singleton ViewModel is never cleared and leaks its dependencies. - Experimental ViewModel scope:
viewModelScopeis marked experimental and may change.
Related
- Android ViewModel:
by viewModel(),activityViewModel(), navigation graph ViewModels. - ViewModel in Compose:
koinViewModel()and navigation ViewModels. - Injected Parameters:
parametersOfand@InjectedParam. - Scopes: scope archetypes and scope lifecycle.
- Definitions: the other definition types.