Does `Nav3` change the opinion on navigating from ...
# compose
u
Does
Nav3
change the opinion on navigating from view model? (Say once a suspend action in vm finishes, one should navigate to screen B) Is it still exposing a
ViewModel.events
(a mutable shared flow of sealed class events) to the ui composable and changing the backstack there? Or is there a better approach?
p
The ViewModel shouldn't know about navigation. Navigation logic should live in a separate class.
👎🏾 1
👎🏿 1
👎 2
u
Wdym “know”? a direct reference to some navigator?
p
Correct, neither the navigator, neither other screens. The moment it knows about that, their reusability gets more limited. Because now you can only use it if there are the other 2 screens present. And ideally you want to be able to drag and drop your screen or container everywhere
u
I dont really care about that as my viewmodels are per screen always The original question is how do you then navigate if you cant have no notion of navigation in the vm? Unless you run viewmodel suspemd functions in ui scope, which breaks with orientation changes
p
Like you described, the ViewModel, delivers an event/state-change that is consumed in the Composable side and bubbled up to the root Composable where NavDisplay is. This is where the navigation manager also lives.
u
not sure if id call that vm doesn’t know about navigation but okay about the event, i assume you mean sharedflow right? dont you worry about ui missing the events somehow, since its racy on orientation change?
p
Right, the missing event dilemma. You can use StateFlow to represent events as a state. Just update the state flow to be null after event consumption.
u
that will mosty likely lead to state clobbering, unless you model it as a list and nobody will bother with that also its too much ceremony for such common thing
m
I wouldn’t use a
StateFlow
nor a
SharedFlow
for events. Use a
Channel
-backed Flow instead (
receiveAsFlow
)
u
isnt sharedflow a channel inside?
m
o
In
Nav3
, we maintain our own backstack, so I believe it is valid to keep it as a
MutableStateFlow
within a repository and expose methods to observe and update it 🤷
Copy code
class InMemoryNavigationRepository : NavigationRepository {

    private val _screens = MutableStateFlow(listOf(Screen.startDestination()))
    override val screens: StateFlow<List<Screen>> = _screens.asStateFlow()

    override fun navigateTo(screen: Screen) {
        _screens.update { backStack ->
            backStack + screen
        }
    }

    override fun navigateBack() {
        _screens.update { backStack ->
            if (backStack.isNotEmpty()) {
                backStack.dropLast(1)
            } else {
                backStack
            }
        }
    }
    ...
}
j
Exposing the backstack, and exposing events to update the backstack, are two separate things
☝️ 1
In a typical app, the majority of navigation events are instantaneous UI events. User clicks a button to go to a new screen etc. Following that principal, for the outlier that is "this work needs to finish before navigating" I think it makes sense to emit an event when that work finishes. I think it's critical that the event is "hey, the work is done", rather than "hey, navigate to a new screen" though. Signalling the work is done leaves it up to the consumer to decide how to handle it, whereas signalling navigation now requires the consumer to know about navigation & also navigate
☝🏿 1
2
☝️ 1
Of course, if your primary navigation model is "this work needs to finish before navigating" then it might make sense to rethink that
Circling back to the original question Has the opinion changed? Not really. All the tools are there for you to manage your backstack and navigation events from your Composables out-of-the-box. It does give you a bit more freedom to move it if you so desire though
u
@Oleksandr Balan if you use that shape, you dont support process restoration (saving the backstack), dont you?
@jack
Of course, if your primary navigation model is "this work needs to finish before navigating" then it might make sense to rethink that
what do you mean by that specifically? a new pattern? since first half of you message sounded like youre arguing for LoginDoneEvent vs GoToHomeEvent, i.e. semantics, but events nonetheless
j
I mean what I said - putting navigation in your ViewModel doesn't make sense for the average app, and care should be taken to keep them separate when you do encounter an exception to the typical "navigate on click" pattern
☝️ 1
u
I'm not sure if I like the dual pattern, sometimes it does route through vm sometimes it doesnr About the event, what kind of primitive are you using for it? A shared flow or something better?
o
@ursus Yea, good point, I use this repository in a demo app, where I did not think about process restoration 🤦‍♂️ But I think it could be supported by saving it to some data-store? Not sure if it is good idea though 🤔
u
@Oleksandr Balan youre just inventing invented stuff, also you dont want to persist backstack to durable storage
j
A SharedFlow isn't a primitive 🤔 I also don't like having different setups for some things, but I don't like turning the exception into the rule even more. As Mark mentioned, a Channel is probably the way to go
☝️ 1
☝🏿 1
u
probably? doesnt sound confident 😀
j
Oh sorry did you want me to just tell you what you must be doing 😒
u
tbh I find it puzzling that this doesnt have a canonical solution and I'm pretty sure most apps will just mutablesharedflow it so I'm not sure how much it matters
j
The problem with inventing a "canonical solution" here is that people will see it as "it's recommended to route all my navigation through a ViewModel", when it's actually not. You're welcome to use a MutableSharedFlow if you'd rather. The best thing to do is always just try it and see
u
Okay I dont really want to get into that as it's mostly academic debate. A fair assesment is I guess it depends on how you view viewmodels and their reusability. For me it's always 1:1 with a screen, they're never shared, I'd use different object if 2 screens need to communicate/share state. The problem of chosing the comm. primitive is you wont really be able to simulate such missed events if you try, which proves nothing
I was under the impression shared flow is built on top of Channel so it seemed redundant but you guys say its not so I'll look into it, thank yous
m
I would recommend you read the article I mentioned earlier. It’s not just a
SharedFlow
💯 1
u
This happens in a design with a type of event that usually has a single subscriber, but intermittently (at startup or during some kind of reconfiguration) there are no subscribers at all, and there is a requirement that all posted events must be retained until a subscriber appears.
okay so this is what you were referencing, right?
👍 1