Nick Kleban
09/04/2023, 4:30 PMStylianos Gakis
09/04/2023, 4:57 PMefemoney
09/04/2023, 5:13 PMjust as you would with any other kind of event. Meaning you bake that into the UIState…My only gripe with this is that events are not state 😅 , so i have a hard time, personally, adding say a boolean “navigateToDetail = true” in my ui model/state. We have also treated nav as an event but slightly different from @Stylianos Gakis. We send the events to a navigator that uses a buffered channel and relays it to the nav controller. Implementation in a nutshell:
class MainNavigator : Navigator {
internal val destinations = Channel<Destination>()
override fun navigate(destination: Destination) {
destinations.trySend(destination)
}
}
@Composable
internal fun CollectFromNavigator(
navController: NavController,
navigator: MainNavigator,
deepLinkLauncher: DeepLinkLauncher,
) {
LaunchedEffect(navigator) {
navigator.destinations
.receiveAsFlow()
.collect { navController.handle(it, deepLinkLauncher) }
}
}
Francesc
09/04/2023, 5:38 PMefemoney
09/04/2023, 5:39 PMFrancesc
09/04/2023, 5:40 PMefemoney
09/04/2023, 5:42 PMsvenjacobs
09/04/2023, 5:43 PMWe’ve had good luck with treating navigation events just as you would with any other kind of event. Meaning you bake that into the UIState, and have the UI react to that state accordingly and report that it’s handled back to the ViewModel. No way to lose an event this way or act on an event twice and other problems like that.We also do it like that. Our UiState is published as a
StateFlow
and may contain a field navigation
for example which can be an enum or a sealed interface if you need to pass additional data for the navigation “event”. Inside the VM we have a MutableStateFlow<Navigation>
which we combine into the UiState with Flow operators. So for example inside suspend functions we can set a value on the MutableState which is reflected in the UiState. Then in Compose we do
LaunchedEffect(uiState.navigation) {
when (uiState.navigation) {
Navigation.Link1 -> onNavigateToLink1()
...
}
viewModel.confirmNavigation()
}
where viewModel.confirmNavigation()
will set the MutableState to a default value (None
) or null
to not handle this event again.Francesc
09/04/2023, 5:45 PMYou’re also not trying to navigate here also, you just send a navigation signal (which is the exact same thing as updating your state with some navigation flag) pending when the collector/ui/whatever is ready to start processing again.but your navigator holds a reference to the NavController, which implies it triggers navigation. If you want to do this, then it is your responsibility to monitor the state of the app and delay the navigation until the app comes back to
svenjacobs
09/04/2023, 5:46 PMthen it is your responsibility to monitor the state of the app and delay the navigation until the app comes back to backgroundyou’ll get this for free when you use
collectAsStateWithLifecyle
on your UiState StateFlow
.Francesc
09/04/2023, 5:46 PMsvenjacobs
09/04/2023, 5:48 PMefemoney
09/04/2023, 5:56 PMbut your navigator holds a reference to the NavController, which implies it triggers navigationI’m sorry but thats not implied anywhere in the code I shared 🤔
responsibility to monitor the state of the app and delay the navigation until the app comes back to foregroundthats what the coroutine scope thats collecting the channel is for, the scope delimits the operations inside it so if you want the collection to last as long as app is on foreground, it goes without saying that you will create a scope thats alive as long as app is in foreground and collect the navigation events in that scope
it is a more convoluted and fragile solution than simply passing the events in the stateHmm hard disagree here but its subjective so no need for a back and forth. Having to “report back” to the presentation logic from every single point where navigation takes place seems more brittle than fire & forget. Using a channel makes it such that if a nav event is handled its automatically removed from the queue and you dont run the risk of say someone not reporting back and suddenly navigating multiple times on subsequent state emissions. Again, navigation is not state, modelling it as state while is not technically wrong, is just a little suspect (… to me)
efemoney
09/04/2023, 5:58 PMyou’ll get this for free when you useNavigation is not a state, a stateflow is quite frankly one the worst model you can use for navigation events. You are in fact better off using a channel or a plain old queue if not in coroutines world.on your UiStatecollectAsStateWithLifecyle
StateFlow
svenjacobs
09/04/2023, 5:59 PMHaving to “report back” to the presentation logic from every single point where navigation takes place seems more brittle than fire & forget.The solution that I presented above has only a single location in Compose for handling navigation, which is the
LaunchedEffect
part and where also the confirmNavigation()
callback is called.efemoney
09/04/2023, 6:02 PMThe solution that I presented above has only a single location…that you have to call for every screen 🙂 and if one forgets to call viewModel.confirmNavigation(), bad things begin to happen all of a sudden, thats the part I was pointing out
svenjacobs
09/04/2023, 6:03 PMNavigation is not a state, a stateflow is quite frankly one the worst model you can use for navigation events.I agree that navigation events should not be state however I believe this is the best trade-off with a MVVM architecture to not introduce other concepts and still use StateFlow, coroutines etc. I think there is no clear architecture recommendation for “how to handle navigation from ViewModel” but I believe if I’m not mistaken that the solution that I presented was recommended by Google engineers but unfortunately I don’t have a source. I might be wrong.
efemoney
09/04/2023, 6:08 PMFrancesc
09/04/2023, 6:09 PMsvenjacobs
09/04/2023, 6:09 PMFrancesc
09/04/2023, 6:10 PMFrancesc
09/04/2023, 6:12 PMefemoney
09/04/2023, 6:12 PMFrancesc
09/04/2023, 6:13 PMFrancesc
09/04/2023, 6:13 PMHaving to “report back” to the presentation logic from every single point where navigation takes place seems more brittle than fire & forget.
Francesc
09/04/2023, 6:14 PMFrancesc
09/04/2023, 6:15 PMefemoney
09/04/2023, 6:19 PMwell, you just suggested fire and forget abovewhich is no different from “update the state & hope someone collects it” 😅 are we delving into technicalities? in both cases your there is no way for your _viewModel_/whatever to know if the navigation actually happened.
Francesc
09/04/2023, 6:20 PMsvenjacobs
09/04/2023, 6:21 PMin both cases your there is no way for your _viewModel_/whatever to know if the navigation actually happenedThat’s true unless you have a listener on your navigation framework and confirm that the new destination is the desired destination. But I would say this is overengineered. At some point you have to trust your framework 😉
efemoney
09/04/2023, 6:21 PMFrancesc
09/04/2023, 6:22 PMsvenjacobs
09/04/2023, 6:22 PMWhereas using a queue (ie channel) guarantees it will be handled once.But here you could “miss” an event if you don’t get the scopes and lifecycles right. Every solution has pros and cons.
Francesc
09/04/2023, 6:23 PMefemoney
09/04/2023, 6:24 PMEvery solution has pros and consExactly. I outlined the one I could not live with personally.
Francesc
09/04/2023, 6:24 PMFrancesc
09/04/2023, 6:24 PMTotally understand but as long as you don’t ack,sure, but now we're talking about a buggy implementation, so all bets are off
Kilian
09/04/2023, 6:31 PMefemoney
09/04/2023, 6:31 PMbut also which screen is in view so know whehter you can or can’t navigateThis is unnecessary. Even more so in navigation compose where you dont have to specify what are “valid directions” (like in fragments) and navigation is handled via “route”s instead.
but also which screen is in view so know wheter you can or can’t navigateCorrect, you can run into this if your presentation logic is not properly scoped to the destination. But at that point you also have other issues and the navigation arch is not one of them.
Nick Kleban
09/05/2023, 8:15 AMdewildte
09/05/2023, 6:14 PMBut theres a risk here if UI does not ack, that you run the navigation multiple times. Whereas using a queue (ie channel) guarantees it will be handled once.Well now that depends on how the navigation is handled from the UI side. Let’s say the
LaunchedEvent
that handles the navigation propagated from the viewmodel sends the acknowledgment first.
For example:
@Composable
fun TestScreen(
navigateAway: () -> Unit,
) {
val viewModel = object {
private val _event = MutableStateFlow(null)
val event: StateFlow<String?> = _event.asStateFlow()
fun eventHandled() {
_event.update { null }
}
}
val event by viewModel.event.collectAsState()
LaunchedEffect(event) {
if (event != null) {
viewModel.eventHandled()
navigateAway()
}
}
}
So this method should ensure the correct order of events.