Skip to main content
Version: 4.2

Android ViewModel & Navigation

Overview

ViewModels are architecture components designed to survive configuration changes and manage UI-related data. Koin provides special support for ViewModels with lifecycle-aware injection.

Key Concepts

  • Survives Configuration Changes - ViewModels persist through rotations and theme changes
  • Scoped to Lifecycle - Tied to Activity, Fragment, or Navigation Graph lifecycle
  • Lazy Creation - Created only when first accessed
  • Shared Instances - Can be shared between Fragments and their host Activity

ViewModel Scope Limitations

danger

Important: ViewModels are created against the root Koin scope and cannot access Activity or Fragment scoped dependencies. This prevents memory leaks as ViewModels outlive Activities and Fragments.

// ❌ This will fail - ViewModel cannot access Activity scope
module {
activityScope {
scoped { ActivityData() }
}

viewModel { MyViewModel(get<ActivityData>()) } // ERROR!
}

// ✅ Correct - Use application scope or pass via parameters
module {
single { AppData() } // Application scope

viewModel { params ->
MyViewModel(
appData = get(),
activitySpecificData = params.get() // Pass as parameter
)
}
}

Declaring ViewModels

The koin-android Gradle module introduces a new viewModel DSL keyword that comes in complement of single and factory, to help declare a ViewModel component and bind it to an Android Component lifecycle. The viewModelOf keyword is also available, to let you declare a ViewModel with its constructor.

val appModule = module {

// ViewModel for Detail View
viewModel { DetailViewModel(get(), get()) }

// or directly with constructor
viewModelOf(::DetailViewModel)

}

Your declared component must at least extends the android.arch.lifecycle.ViewModel class. You can specify how you inject the constructor of the class and use the get() function to inject dependencies.

info

The viewModel/viewModelOf keyword helps to declare a factory instance of ViewModel. This instance will be handled by internal ViewModelFactory and reattach ViewModel instance if needed. It also will let inject parameters.

Injecting your ViewModel

To inject a ViewModel in an Activity, Fragment or Service use:

  • by viewModel() - lazy delegate property to inject a ViewModel into a property
  • getViewModel() - directly get the ViewModel instance
class DetailActivity : AppCompatActivity() {

// Lazy inject ViewModel
val detailViewModel: DetailViewModel by viewModel()
}
note

ViewModel key is calculated against Key and/or Qualifier

Activity Shared ViewModel

One ViewModel instance can be shared between Fragments and their host Activity.

To inject a shared ViewModel in a Fragment use:

  • by activityViewModel() - lazy delegate property to inject shared ViewModel instance into a property
  • getActivityViewModel() - directly get the shared ViewModel instance
note

The sharedViewModel is deprecated in favor of activityViewModel() functions. The naming of this last one is more explicit.

Just declare the ViewModel only once:

val weatherAppModule = module {

// WeatherViewModel declaration for Weather View components
viewModel { WeatherViewModel(get(), get()) }
}

Note: a qualifier for a ViewModel will be handled as a ViewModel's Tag

And reuse it in Activity and Fragments:

class WeatherActivity : AppCompatActivity() {

/*
* Declare WeatherViewModel with Koin and allow constructor dependency injection
*/
private val weatherViewModel by viewModel<WeatherViewModel>()
}

class WeatherHeaderFragment : Fragment() {

/*
* Declare shared WeatherViewModel with WeatherActivity
*/
private val weatherViewModel by activityViewModel<WeatherViewModel>()
}

class WeatherListFragment : Fragment() {

/*
* Declare shared WeatherViewModel with WeatherActivity
*/
private val weatherViewModel by activityViewModel<WeatherViewModel>()
}

Passing Parameters to the Constructor

The viewModel keyword API is compatible with injection parameters.

In the module:

val appModule = module {

// ViewModel for Detail View with id as parameter injection
viewModel { parameters -> DetailViewModel(id = parameters.get(), get(), get()) }
// ViewModel for Detail View with id as parameter injection, resolved from graph
viewModel { DetailViewModel(get(), get(), get()) }
// or autowire DSL
viewModelOf(::DetailViewModel)
}

From the injection call site:

class DetailActivity : AppCompatActivity() {

val id : String // id of the view

// Lazy inject ViewModel with id parameter
val detailViewModel: DetailViewModel by viewModel{ parametersOf(id)}
}

SavedStateHandle Injection (3.3.0)

Add a new property typed SavedStateHandle to your constructor to handle your ViewModel state:

class MyStateVM(val handle: SavedStateHandle, val myService : MyService) : ViewModel()

In Koin module, just resolve it with get() or with parameters:

viewModel { MyStateVM(get(), get()) }

or with autowire DSL:

viewModelOf(::MyStateVM)

To inject a state ViewModel in a Activity,Fragment use:

  • by viewModel() - lazy delegate property to inject state ViewModel instance into a property
  • getViewModel() - directly get the state ViewModel instance
class DetailActivity : AppCompatActivity() {

// MyStateVM viewModel injected with SavedStateHandle
val myStateVM: MyStateVM by viewModel()
}
info

All stateViewModel functions are deprecated. You can just use the regular viewModel function to inject a SavedStateHandle

You can scope a ViewModel instance to your Navigation graph. Just retrieve with by koinNavGraphViewModel(). You just need your graph id.

class NavFragment : Fragment() {

val mainViewModel: NavViewModel by koinNavGraphViewModel(R.id.my_graph)

}

ViewModel Scope API

see all API to be used for ViewModel and Scopes: ViewModel Scope

ViewModel Generic API

Koin provides some "under the hood" API to directly tweak your ViewModel instance. The available functions are viewModelForClass for ComponentActivity and Fragment:

ComponentActivity.viewModelForClass(
clazz: KClass<T>,
qualifier: Qualifier? = null,
owner: ViewModelStoreOwner = this,
state: BundleDefinition? = null,
key: String? = null,
parameters: ParametersDefinition? = null,
): Lazy<T>
note

This function is still using state: BundleDefinition, but will convert it to CreationExtras

Note that you can have access to the top level function, callable from anywhere:

fun <T : ViewModel> getLazyViewModelForClass(
clazz: KClass<T>,
owner: ViewModelStoreOwner,
scope: Scope = GlobalContext.get().scopeRegistry.rootScope,
qualifier: Qualifier? = null,
state: BundleDefinition? = null,
key: String? = null,
parameters: ParametersDefinition? = null,
): Lazy<T>

ViewModel API - Java Compat

Java compatibility must be added to your dependencies:

// Java Compatibility
implementation "io.insert-koin:koin-android-compat:$koin_version"

You can inject the ViewModel instance to your Java codebase by using viewModel() or getViewModel() static functions from ViewModelCompat:

@JvmOverloads
@JvmStatic
@MainThread
fun <T : ViewModel> getViewModel(
owner: ViewModelStoreOwner,
clazz: Class<T>,
qualifier: Qualifier? = null,
parameters: ParametersDefinition? = null
)

State Management Patterns

Using StateFlow

class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {

private val _userState = MutableStateFlow<UiState<User>>(UiState.Loading)
val userState: StateFlow<UiState<User>> = _userState.asStateFlow()

init {
loadUser()
}

private fun loadUser() {
viewModelScope.launch {
_userState.value = UiState.Loading
try {
val user = userRepository.getUser()
_userState.value = UiState.Success(user)
} catch (e: Exception) {
_userState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}

// Koin module
val viewModelModule = module {
viewModelOf(::UserViewModel)
singleOf(::UserRepository)
}

// Collecting in Activity/Fragment
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showUser(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
}
}

With SavedStateHandle

class ProductViewModel(
private val savedStateHandle: SavedStateHandle,
private val productRepository: ProductRepository
) : ViewModel() {

companion object {
private const val KEY_PRODUCT_ID = "product_id"
private const val KEY_QUANTITY = "quantity"
}

// Persisted across process death
val productId: String = savedStateHandle[KEY_PRODUCT_ID] ?: ""

var quantity: Int
get() = savedStateHandle[KEY_QUANTITY] ?: 1
set(value) {
savedStateHandle[KEY_QUANTITY] = value
}

private val _product = MutableStateFlow<Product?>(null)
val product: StateFlow<Product?> = _product.asStateFlow()

init {
loadProduct()
}

private fun loadProduct() {
viewModelScope.launch {
_product.value = productRepository.getProduct(productId)
}
}

fun incrementQuantity() {
quantity++
}
}

// Module
val productModule = module {
viewModelOf(::ProductViewModel)
}

// Usage - SavedStateHandle automatically injected
class ProductActivity : AppCompatActivity() {
private val viewModel: ProductViewModel by viewModel()
}

ViewModel Lifecycle

Activity ViewModel Lifecycle

class MyViewModel : ViewModel() {
init {
Log.d("VM", "ViewModel created")
}

override fun onCleared() {
super.onCleared()
Log.d("VM", "ViewModel destroyed")
}
}

// Lifecycle:
// 1. Activity onCreate() → ViewModel created (first time)
// 2. Activity onDestroy() (rotation) → ViewModel survives
// 3. New Activity onCreate() → Same ViewModel instance
// 4. Activity finish() → ViewModel.onCleared() called

Fragment ViewModel Lifecycle

class MyFragment : Fragment() {
// Fragment-scoped ViewModel
private val fragmentVM: MyViewModel by viewModel()

// Activity-scoped ViewModel (shared)
private val activityVM: SharedViewModel by activityViewModel()

// Lifecycle:
// - fragmentVM: Created when fragment is created, destroyed when fragment is destroyed
// - activityVM: Created with activity, destroyed when activity is destroyed
}
class Fragment1 : Fragment() {
private val graphVM: GraphViewModel by koinNavGraphViewModel(R.id.my_graph)
// Created when first fragment in graph accesses it
// Destroyed when navigation graph is popped
}

class Fragment2 : Fragment() {
private val graphVM: GraphViewModel by koinNavGraphViewModel(R.id.my_graph)
// Same instance as Fragment1
}

Testing ViewModels

Unit Testing

class UserViewModelTest : KoinTest {

@get:Rule
val koinTestRule = KoinTestRule.create {
modules(
module {
viewModelOf(::UserViewModel)
single<UserRepository> { FakeUserRepository() }
}
)
}

private val viewModel: UserViewModel by inject()

@Test
fun `load user updates state`() = runTest {
// Given
val expectedUser = User("1", "Test User")

// When
viewModel.loadUser("1")
advanceUntilIdle()

// Then
val state = viewModel.userState.value
assertTrue(state is UiState.Success)
assertEquals(expectedUser, (state as UiState.Success).data)
}
}

Testing with Mock Repository

@Test
fun `load user handles error`() = runTest {
val mockRepo = mockk<UserRepository> {
coEvery { getUser(any()) } throws Exception("Network error")
}

val viewModel = UserViewModel(mockRepo)

viewModel.loadUser("1")
advanceUntilIdle()

val state = viewModel.userState.value
assertTrue(state is UiState.Error)
}

Testing Shared ViewModel

@Test
fun `shared ViewModel maintains state across fragments`() {
val scenario = launchFragmentInContainer<MyFragment>()

scenario.onFragment { fragment ->
val viewModel = fragment.activityViewModel<SharedViewModel>()
viewModel.updateData("test data")

// Navigate to another fragment
// ViewModel state should persist
}
}

Best Practices

1. Use Constructor Injection

// ✅ Good - Constructor injection
class UserViewModel(
private val repository: UserRepository,
private val analytics: AnalyticsService
) : ViewModel()

module {
viewModelOf(::UserViewModel)
}

// ❌ Bad - Field injection with KoinComponent
class UserViewModel : ViewModel(), KoinComponent {
private val repository: UserRepository by inject()
}

2. Expose Immutable State

// ✅ Good - Immutable public state
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow() // Read-only
}

// ❌ Bad - Mutable public state
class MyViewModel : ViewModel() {
val state = MutableStateFlow<UiState>(UiState.Loading) // Can be modified externally!
}

3. Handle Configuration Changes

// ✅ Good - StateFlow survives rotation
class MyViewModel : ViewModel() {
private val _data = MutableStateFlow<String>("")
val data: StateFlow<String> = _data.asStateFlow()

// Data persists across rotation
}

// ❌ Bad - LiveData without SavedStateHandle
class MyViewModel : ViewModel() {
val data = MutableLiveData<String>()
// Lost on process death
}

// ✅ Better - With SavedStateHandle
class MyViewModel(
private val handle: SavedStateHandle
) : ViewModel() {
val data: LiveData<String> = handle.getLiveData("data")
// Survives process death
}

4. Clean Up Resources

class LocationViewModel(
private val locationService: LocationService
) : ViewModel() {

init {
locationService.startTracking()
}

override fun onCleared() {
locationService.stopTracking() // Clean up
super.onCleared()
}
}

5. Use viewModelScope for Coroutines

class DataViewModel(
private val repository: DataRepository
) : ViewModel() {

fun loadData() {
viewModelScope.launch { // Automatically cancelled when ViewModel is cleared
val data = repository.fetchData()
_state.value = UiState.Success(data)
}
}
}

6. Pass Activity-Specific Data as Parameters

// ✅ Good - Pass activity data as parameter
class DetailViewModel(
private val itemId: String, // From activity
private val repository: ItemRepository // From Koin
) : ViewModel()

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

class DetailActivity : AppCompatActivity() {
private val itemId by lazy { intent.getStringExtra("ITEM_ID")!! }
private val viewModel: DetailViewModel by viewModel { parametersOf(itemId) }
}

// ❌ Bad - Don't inject activity-scoped data
module {
activityScope {
scoped { ActivityData() }
}

viewModel { DetailViewModel(get<ActivityData>()) } // ERROR!
}

Common Patterns

Master-Detail Flow

// Shared ViewModel between list and detail fragments
class ProductsViewModel(
private val repository: ProductRepository
) : ViewModel() {

private val _products = MutableStateFlow<List<Product>>(emptyList())
val products: StateFlow<List<Product>> = _products.asStateFlow()

private val _selectedProduct = MutableStateFlow<Product?>(null)
val selectedProduct: StateFlow<Product?> = _selectedProduct.asStateFlow()

fun selectProduct(product: Product) {
_selectedProduct.value = product
}
}

// List Fragment
class ProductListFragment : Fragment() {
private val viewModel: ProductsViewModel by activityViewModel()

fun onProductClick(product: Product) {
viewModel.selectProduct(product)
// Navigate to detail
}
}

// Detail Fragment
class ProductDetailFragment : Fragment() {
private val viewModel: ProductsViewModel by activityViewModel()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewModel.selectedProduct.collect { product ->
product?.let { showProductDetails(it) }
}
}
}
}

Form State Management

class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {

var name: String
get() = savedStateHandle["name"] ?: ""
set(value) { savedStateHandle["name"] = value }

var email: String
get() = savedStateHandle["email"] ?: ""
set(value) { savedStateHandle["email"] = value }

fun isValid(): Boolean {
return name.isNotBlank() && email.contains("@")
}

fun submit() {
if (isValid()) {
// Submit form
}
}
}

Troubleshooting

Issue: ViewModel Not Surviving Rotation

Problem: ViewModel is recreated on rotation.

Solution:

// ❌ Bad - Using factory instead of viewModel
module {
factory { MyViewModel(get()) } // New instance each time!
}

// ✅ Good - Use viewModel DSL
module {
viewModel { MyViewModel(get()) } // Survives rotation
}

Issue: Shared ViewModel Not Working

Problem: Fragments don't share the same ViewModel instance.

Solution:

// ❌ Bad - Each fragment gets its own instance
class MyFragment : Fragment() {
private val viewModel: SharedViewModel by viewModel() // Fragment-scoped
}

// ✅ Good - Share with activity
class MyFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModel() // Activity-scoped
}

Issue: Cannot Access Activity Scope

Problem: NoBeanDefFoundException when ViewModel tries to access activity-scoped dependency.

Solution:

// ❌ Bad - Trying to access activity scope
module {
activityScope {
scoped { ActivityData() }
}
viewModel { MyViewModel(get<ActivityData>()) } // ERROR!
}

// ✅ Good - Pass as parameter or use application scope
module {
viewModel { params ->
MyViewModel(activityData = params.get())
}
}

class MyActivity : AppCompatActivity() {
private val activityData = ActivityData()
private val viewModel: MyViewModel by viewModel { parametersOf(activityData) }
}

Summary

Key points for ViewModels with Koin:

  • Use viewModel DSL for ViewModel declarations
  • Survives configuration changes automatically
  • Cannot access Activity/Fragment scopes to prevent leaks
  • Pass activity-specific data via parametersOf()
  • Share between fragments using activityViewModel()
  • SavedStateHandle automatically injected with get()
  • Use StateFlow for reactive state management
  • Clean up in onCleared() when needed
  • Test easily with Koin Test framework

Next Steps