arkivanov / Decompose

Kotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing (navigation) and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.)
https://arkivanov.github.io/Decompose
Apache License 2.0
2.25k stars 85 forks source link

ViewModel reinitialized on navigation pop with Children stack animation #765

Closed AndrazP closed 2 months ago

AndrazP commented 3 months ago

There is an issue with the ViewModel lifecycle when an animation is set to Children stack. If animation is set, the ViewModel is correctly cleared on navigation pop, but it is then unexpectedly recreated again.

Example of navigating from the list to the details screen and back:

  1. List viewModel init()
  2. nav.push(Config.Details...)
  3. Details viewModel init()
  4. navigation.pop()
  5. Details viewModel onCleared()
  6. Details viewModel init() // Issue

Minimal reproducible example project

arkivanov commented 2 months ago

Thank you for the report and the reproducer. It appears to be a bug in the documentation. The official navigation clears the child ViewModelStore when the screen is removed from the composition, whereas Decompose clears on pop. As a result, a new instance of the child ViewModel is created when the child screen recomposes because of the exit animation.

Please use the following function for creating ViewModels. I will also need to update the docs.

@Composable
inline fun <reified T : ViewModel> rememberViewModel(): T {
    var vm by remember { mutableStateOf<T?>(null) }

    if (vm == null) {
        vm = koinViewModel<T>()
    }

    return requireNotNull(vm)
}
arkivanov commented 2 months ago

Please also note that the whole purpose of Decompose-ViewModel interop is to provide a temporary solution when migrating from the official navigation-compose library. It is advised to eventually get rid of ViewModels completely and start using InstanceKeeper API.

AndrazP commented 2 months ago

Thanks, that solves it!

yuvaraj119 commented 2 months ago

Thank you for the report and the reproducer. It appears to be a bug in the documentation. The official navigation clears the child ViewModelStore when the screen is removed from the composition, whereas Decompose clears on pop. As a result, a new instance of the child ViewModel is created when the child screen recomposes because of the exit animation.

Please use the following function for creating ViewModels. I will also need to update the docs.

@Composable
inline fun <reified T : ViewModel> rememberViewModel(): T {
    var vm by remember { mutableStateOf<T?>(null) }

    if (vm == null) {
        vm = koinViewModel<T>()
    }

    return requireNotNull(vm)
}

What if i have to pass a constructor parameter to viewmodel? something like this First approach: class XScreenViewModel(val iXScreen: IXScreen):ViewModel() { }

OR Second approach If i make my viewmodel component will that cause issue?

class SplashScreenViewModel(componentContext: ComponentContext) : ViewModel(), IXScreen, ComponentContext by componentContext {

best would be First if it works. This will isolate the whole decompose component from actual code and later at somepoint if we have to switch the Navigation it will be easy to do.