Back to Koin

ViewModel in Compose

docs/reference/koin-compose/compose-viewmodel.md

4.2.27.6 KB
Original Source

ViewModel in Compose

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. :::

Setup

kotlin
// 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. :::

Declaring ViewModels

Compiler Plugin DSL

kotlin
class UserViewModel(
    private val repository: UserRepository
) : ViewModel()

val appModule = module {
    viewModel<UserViewModel>()
}

Annotations

kotlin
@KoinViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel()

Classic DSL

kotlin
val appModule = module {
    viewModelOf(::UserViewModel)
    // or with lambda
    viewModel { UserViewModel(get()) }
}

ViewModel Injection APIs

koinViewModel() - Basic Injection

The primary API for injecting ViewModels in Compose:

kotlin
@Composable
fun UserScreen() {
    val viewModel = koinViewModel<UserViewModel>()
    // Use viewModel...
}

Best practice - inject as default parameter for testability:

kotlin
@Composable
fun UserScreen(
    viewModel: UserViewModel = koinViewModel()
) {
    val state by viewModel.state.collectAsState()
    // UI...
}

koinNavViewModel() - With Navigation Arguments

When using Navigation Compose, use koinNavViewModel() to automatically receive navigation arguments via SavedStateHandle:

kotlin
// 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
}

koinActivityViewModel() - Activity-Scoped (Android)

Share a ViewModel across all Composables within the same Activity:

kotlin
@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. :::

sharedKoinViewModel() - Navigation Graph Scoped

Share a ViewModel within a navigation graph (experimental):

kotlin
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)
    }
}

ViewModel with Parameters

Using @InjectedParam

Mark runtime parameters with @InjectedParam:

kotlin
class DetailViewModel(
    @InjectedParam val itemId: String,
    private val repository: DetailRepository
) : ViewModel()

// Compiler Plugin DSL
val appModule = module {
    viewModel<DetailViewModel>()
}

Inject with parameters:

kotlin
@Composable
fun DetailScreen(itemId: String) {
    val viewModel = koinViewModel<DetailViewModel> {
        parametersOf(itemId)
    }
}

Use the key parameter to create distinct ViewModel instances per identifier (e.g., navigation arguments):

kotlin
@Composable
fun DetailScreen(newsId: String) {
    val viewModel = koinViewModel<DetailViewModel>(key = newsId) {
        parametersOf(newsId)
    }
}

The key ensures each unique newsId gets its own ViewModel instance, which is important for screens that can be on the back stack with different arguments.

Classic DSL with Parameters

kotlin
val appModule = module {
    viewModel { params ->
        DetailViewModel(
            itemId = params.get(),
            repository = get()
        )
    }
}

SavedStateHandle

Koin automatically provides SavedStateHandle to ViewModels:

kotlin
@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("") }
}
kotlin
val appModule = module {
    viewModel<MyViewModel>()  // SavedStateHandle injected automatically
}

:::info SavedStateHandle is injected from either ViewModel CreationExtras or Navigation BackStackEntry, depending on context. :::

ViewModel Scope

Scope dependencies to ViewModel lifecycle using viewModelScope:

Compiler Plugin DSL

kotlin
val appModule = module {
    viewModelScope {
        scoped<UserCache>()
        scoped<UserRepository>()
        viewModel<UserViewModel>()
    }
}

Annotations

kotlin
@ViewModelScope
class UserCache

@ViewModelScope
class UserRepository(private val cache: UserCache)

@KoinViewModel
@ViewModelScope
class UserViewModel(private val repository: UserRepository) : ViewModel()

Classic DSL

kotlin
val appModule = module {
    viewModelScope {
        scoped { UserCache() }
        scoped { UserRepository(get()) }
        viewModel { UserViewModel(get()) }
    }
}

:::caution Requires the viewModelScopeFactory() option Declaring a ViewModel inside viewModelScope { } requires enabling options(viewModelScopeFactory()) in your Koin configuration — otherwise koinViewModel() fails with No definition found … on scope '['_root_']'. See ViewModel Scope for details. :::

Quick Reference

APIUse CasePackage
koinViewModel()Basic ViewModel injectionkoin-compose-viewmodel
koinNavViewModel()With Navigation argumentskoin-compose-viewmodel-navigation
koinActivityViewModel()Shared across Activity (Android)koin-androidx-compose
sharedKoinViewModel()Shared within nav graphkoin-compose-viewmodel-navigation

Best Practices

  1. Inject as default parameters - enables testing without Koin

    kotlin
    @Composable
    fun MyScreen(viewModel: MyViewModel = koinViewModel())
    
  2. Use koinNavViewModel() with Navigation - automatic argument handling

  3. Prefer viewModelScope for ViewModel-specific dependencies - clean lifecycle management

  4. Don't inject ViewModels in callbacks - inject at Composable level

    kotlin
    // Bad
    Button(onClick = { val vm = koinViewModel<MyVM>() })
    
    // Good
    val vm = koinViewModel<MyVM>()
    Button(onClick = { vm.doSomething() })
    

Next Steps