Mackovich
Mackovich

Reputation: 3593

NavigationViewModel gets unexpectedly cleared when a screen is popped from the back stack

Let's assume I have two screens:

When I send a NavigationUiEvent.ShowScreenB to the NavigationModel, the NavigationModel emits that event into a SharedFlow.

That flow is collected via collectAsStateWithLifecycle in a Navigator composable function. Then that Navigator uses the NavHostController to navigate.

If I hit the back button, the app pops Screen B and I am back to Screen A.

Strangely, NavigationModel#onCleared is invoked during the popping. I picked that stack trace manually:

com.myapp.presentation.viewmodels.NavigationViewModel.onCleared(NavigationViewModel.kt:59)
androidx.lifecycle.ViewModel.clear(ViewModel.java:202)
androidx.lifecycle.ViewModelStore.clear(ViewModelStore.kt:69)
androidx.navigation.NavControllerViewModel.clear(NavControllerViewModel.kt:33)
androidx.navigation.NavController$NavControllerNavigatorState.markTransitionComplete(NavController.kt:359)
androidx.navigation.compose.ComposeNavigator.onTransitionComplete(ComposeNavigator.kt:8androidx.navigation.compose.NavHostKt$NavHost$15.invokeSuspend(NavHost.kt:314)

Because of this, the NavigationViewModel scope & flow are cancelled, and won't accept any new NavigationUiEvent whatsoever so I am stuck at Screen A.


Please have a look at my code ⤵️

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    Box(modifier = Modifier.fillMaxSize()) {
                        SetupAppNavigation()
                    }
                }
            }
        }
    }
}

@Composable
fun SetupAppNavigation() {
    val navController = rememberNavController()

    Navigator(navHostController = navController) // sits on top of the nav host

    NavHost(
        navController = navController,
        startDestination = "screen_a",
    ) {
        composable(route = "screen_a") {
            ScreenARoute()
        }

        composable(route = "screen_b") {
            ScreenBRoute()
        }
    }
}

@Composable
fun Navigator(
    navHostController: NavHostController,
) {
    val navigationViewModel: NavigationViewModel = koinViewModel()
    val navDirection: NavigationUiEvent? by navigationViewModel.state.collectAsStateWithLifecycle(initialValue = null)

    when (val nav = navDirection) {
        is NavigationUiEvent.ScreenB -> navHostController.navigate(route = "screen_b")
        null -> Unit // Nothing to do here
    }
}

class NavigationViewModel internal constructor() : ViewModel(), NavigationUiEventHandler, KoinComponent {

    private val module: Module by lazy {
        module {
            single { this@NavigationViewModel } bind NavigationUiEventHandler::class
        }
    }

    private val events = MutableSharedFlow<NavigationUiEvent>()

    val state: SharedFlow<NavigationUiEvent> = events
        .onStart { getKoin().loadModules(listOf(module)) }
        .onCompletion { getKoin().unloadModules(listOf(module)) }
        .shareIn(
            scope = viewModelScope,
            started = SharingStarted.Eagerly,
        )

    override fun handleEvent(event: NavigationUiEvent) {
        viewModelScope.launch {
            events.emit(event)
        }
    }

    override fun onCleared() {
        super.onCleared()
        println("DEBUG >> NavigationViewModel CLEARED so YEAH GOODBYE !")
    }
}


# KOIN INJECTION #

val viewModelsModule = module {
    viewModeOf(::NavigationViewModel)
    viewModeOf(::ScreenAViewModel)
    viewModeOf(::ScreenBViewModel)
}

class MyApp: Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger()
            androidContext(this@ MyApp)
            modules(
                listOf(
                    viewModelsModule,
                    ...
                )
            )
        }
    }
}

The app prints...

DEBUG >> NavigationViewModel CLEARED so YEAH GOODBYE !

... when I am going back to Screen A.

Why ?

I honestly don't understand why NavControllerViewModel (from the stack trace) would clear my NavigationViewModel even though it isn't explicitly scoped to a particular screen as part of my navigation graph.

Upvotes: 0

Views: 56

Answers (0)

Related Questions