Reputation: 3593
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