docs/reference/koin-compose/compose-viewmodel.md
Koin provides several APIs for injecting ViewModels in Compose applications. This guide covers all ViewModel injection patterns.
:::info For declaring ViewModels in modules, see Core ViewModel. This page focuses on retrieving ViewModels in Compose. :::
// Compose Multiplatform (or Android)
implementation("io.insert-koin:koin-compose-viewmodel:$koin_version")
// Android convenience (includes koin-compose + koin-compose-viewmodel)
implementation("io.insert-koin:koin-androidx-compose:$koin_version")
// With Navigation integration
implementation("io.insert-koin:koin-compose-viewmodel-navigation:$koin_version")
:::info
All ViewModel APIs are in koin-compose-viewmodel. The koin-androidx-compose package includes it automatically.
:::
class UserViewModel(
private val repository: UserRepository
) : ViewModel()
val appModule = module {
viewModel<UserViewModel>()
}
@KoinViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel()
val appModule = module {
viewModelOf(::UserViewModel)
// or with lambda
viewModel { UserViewModel(get()) }
}
The primary API for injecting ViewModels in Compose:
@Composable
fun UserScreen() {
val viewModel = koinViewModel<UserViewModel>()
// Use viewModel...
}
Best practice - inject as default parameter for testability:
@Composable
fun UserScreen(
viewModel: UserViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsState()
// UI...
}
When using Navigation Compose, use koinNavViewModel() to automatically receive navigation arguments via SavedStateHandle:
// Route with arguments
NavHost(navController, startDestination = "list") {
composable("detail/{itemId}") { backStackEntry ->
DetailScreen()
}
}
// ViewModel receives arguments automatically
class DetailViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val itemId: String = savedStateHandle["itemId"] ?: ""
}
@Composable
fun DetailScreen(
viewModel: DetailViewModel = koinNavViewModel()
) {
// viewModel.itemId is populated from navigation arguments
}
Share a ViewModel across all Composables within the same Activity:
@Composable
fun ScreenA() {
// Same instance across Activity
val sharedVM = koinActivityViewModel<SharedViewModel>()
}
@Composable
fun ScreenB() {
// Same instance as ScreenA
val sharedVM = koinActivityViewModel<SharedViewModel>()
}
:::note
Available in koin-androidx-compose starting from version 4.1.
:::
Share a ViewModel within a navigation graph (experimental):
navigation<Route.BookGraph>(startDestination = Route.BookList) {
composable<Route.BookList> { backStackEntry ->
val sharedVM = backStackEntry.sharedKoinViewModel<BookSharedViewModel>(navController)
BookListScreen(sharedVM)
}
composable<Route.BookDetail> { backStackEntry ->
// Same instance within BookGraph
val sharedVM = backStackEntry.sharedKoinViewModel<BookSharedViewModel>(navController)
BookDetailScreen(sharedVM)
}
}
Mark runtime parameters with @InjectedParam:
class DetailViewModel(
@InjectedParam val itemId: String,
private val repository: DetailRepository
) : ViewModel()
// Compiler Plugin DSL
val appModule = module {
viewModel<DetailViewModel>()
}
Inject with parameters:
@Composable
fun DetailScreen(itemId: String) {
val viewModel = koinViewModel<DetailViewModel> {
parametersOf(itemId)
}
}
val appModule = module {
viewModel { params ->
DetailViewModel(
itemId = params.get(),
repository = get()
)
}
}
Koin automatically provides SavedStateHandle to ViewModels:
@KoinViewModel
class MyViewModel(
private val handle: SavedStateHandle,
private val repository: UserRepository
) : ViewModel() {
// Access navigation arguments
val userId: String? = handle["userId"]
// Persist state across process death
var query by handle.saveable { mutableStateOf("") }
}
val appModule = module {
viewModel<MyViewModel>() // SavedStateHandle injected automatically
}
:::info
SavedStateHandle is injected from either ViewModel CreationExtras or Navigation BackStackEntry, depending on context.
:::
Scope dependencies to ViewModel lifecycle using viewModelScope:
val appModule = module {
viewModelScope {
scoped<UserCache>()
scoped<UserRepository>()
viewModel<UserViewModel>()
}
}
@ViewModelScope
class UserCache
@ViewModelScope
class UserRepository(private val cache: UserCache)
@KoinViewModel
@ViewModelScope
class UserViewModel(private val repository: UserRepository) : ViewModel()
val appModule = module {
viewModelScope {
scoped { UserCache() }
scoped { UserRepository(get()) }
viewModel { UserViewModel(get()) }
}
}
| API | Use Case | Package |
|---|---|---|
koinViewModel() | Basic ViewModel injection | koin-compose-viewmodel |
koinNavViewModel() | With Navigation arguments | koin-compose-viewmodel-navigation |
koinActivityViewModel() | Shared across Activity (Android) | koin-androidx-compose |
sharedKoinViewModel() | Shared within nav graph | koin-compose-viewmodel-navigation |
Inject as default parameters - enables testing without Koin
@Composable
fun MyScreen(viewModel: MyViewModel = koinViewModel())
Use koinNavViewModel() with Navigation - automatic argument handling
Prefer viewModelScope for ViewModel-specific dependencies - clean lifecycle management
Don't inject ViewModels in callbacks - inject at Composable level
// Bad
Button(onClick = { val vm = koinViewModel<MyVM>() })
// Good
val vm = koinViewModel<MyVM>()
Button(onClick = { vm.doSomething() })