Hey everyone :wave: I have a `Nav3` question about...
# compose
k
Hey everyone πŸ‘‹ I have a
Nav3
question about replicating
singleInstance
+
onNewIntent
behavior inside an
entryProvider
. The goal: a destination should only ever exist once in the back stack, and when something tries to navigate to it again with new data, the already-rendered screen should receive that updated data (similar to how
onNewIntent
works for a
singleInstance
Activity). My current approach: - A
NavParentViewModel
(scoped above the
NavDisplay
) holds a
Map<NavKey, Channel<T>>
, one
Channel
per entry type - When navigating to a destination that's already in the back stack, instead of pushing a new key, I send the new data into that key's
Channel
- The composable for that entry collects from its
Channel
and updates its state accordingly Is this a reasonable pattern for
Nav3
, or is there a recommended/idiomatic way to handle this? Would love to know if there's something cleaner, maybe using the
NavEntry
metadata or a different state-sharing mechanism, before I commit to this approach. Thanks!
i
What does "receive that updated data" mean? Generally, there's a 1:1 relationship between the data you receive in onNewIntent and the data class representing a screen, so, by definition, that exact instance either already has the correct state or it is a brand new instance that isn't on the back stack already?
If you're trying to tie the state to only part of the fields of a class, there's already a mechanism for that: the
contentKey
of your
entry
- all of the state is tied to the
contentKey
, so as long as that is the same, you'll get the exact same state, ViewModels, etc. and just magically get the new
key
instance itself, no
Channel
or anything like that required
But keep in mind with freeform multi-window devices, you really shouldn't ever be using
singleInstance
kind of mechanics in the first place: that almost always means you are making bad assumptions, IMO
So maybe it would be helpful to explain the user experience you are trying to achieve first, then we can talk through if that even makes sense before going into the implementation of that user experience
k
Thanks for the response. Let me clarify the actual use case first. I have an
app-nav-graph
at the root, with a
home-nav-graph
nested inside it that drives a home screen with a bottom nav bar and multiple nested screens. Some of those screens contain links that deep-link back into the
home-nav-graph
itself. I have a centralized
DeepLinkResolver
that constructs the route and triggers navigation β€” and when the target is
home-nav-graph
, I want the existing instance to stay alive and react to the new route info rather than pushing a fresh instance with a new ViewModel. I did look at
contentKey
as a solution β€” keep it constant so the ViewModel survives, and pass the updated route params to the screen and to it's view-model via
LaunchedEffect(param)
. But this creates a consume-once problem: if I don't clear the params after handling them, every time the user leaves
home-nav-graph
and comes back,
LaunchedEffect(param)
re-fires with the stale deep link data and triggers unintended navigation again. So I'd need to explicitly null out the param in the ViewModel after consumption, which adds boilerplate and makes state harder to reason about. That's the reason behind my
ChannelFlow
idea I am just exploring if this would be a better alternative. Let me know if I made any mistake in my approach.
i
Can you explain what led you to such a complicated setup? And can you explain what you mean by nav graph? That isn't a term in Nav3
k
I am sorry to create the confusion I very much used to thinking in terms of navigation as graphs like in
Nav2
. In
Nav3
terms, what I mean is a nested
NavDisplay
with its own back stack, scoped to the home screen. The setup is complicated because of the my product managers requirement πŸ˜‚ and making things backend driven.
The idea is making our app backend-driven, where the server controls what's on screen and what happens on tap β€” including launching deep links. Those deep links are resolved by a centralized
DeeplinkResolver
, and sometimes they target a section of the nested
NavDisplay
that's already on the back stack, which is where the "keep the existing instance alive and update it" problem comes from.
i
Have you looked at the nav3-recipes repository? It has had an example of a conditional bottom nav (now made much easier by Scene Decorators) for quite some time, no nested NavDisplay required at all. That's always been way, way, way more than you ever need. Hence my question on how you got to deciding that was the right choice for your use case as it certainly sounds like everything would be way easier without that style of implementation
That repository also has a deep link recipe, which also does something similar in centralizing the deep link handling (it just changes the back stack), which obviously is way easier if you follow what the other recipes are doing and use a single NavDisplay (keep in mind that you can still have multiple back stacks you
+
together even if you have one NavDisplay)
k
Thanks for the pointers! I took the pragmatic call for multiple
NavDisplay's
because of migration and it let the team work more easliy on different sections in parallel. I had read the multiple back stacks recipe but not the deep link one, I'll check that out next. Even with a single
NavDisplay
though, the core question remains: when a deep link targets something already on the back stack, swapping the key creates a new ViewModel. Using a constant
contentKey
preserves it but leaves me with the stale param problem. Curious to see how the deep link recipe handles that.
i
Okay, so answer my first question:
What does "receive that updated data" mean? Generally, there's a 1:1 relationship between the data you receive in onNewIntent and the data class representing a screen, so, by definition, that exact instance either already has the correct state or it is a brand new instance that isn't on the back stack already?
k
Yes yes, you're right, my bad this is a problem that only arises in my particular setup. I've got multiple
NavDisplays
and
per-tab back stacks inside one of my NavDisplays
, which is what's breaking the 1:1 for me, in a flat back stack your framing is exactly right. The distinction I was trying to draw is in how the new data arrives in my setup: Activity:
Copy code
Screen A (ViewModel survives cause of singleInstance)
  onCreate()    β†’ Intent
  onNewIntent() β†’ new Intent   // delivered as a one-shot callback
Compose / Nav3:
Copy code
Screen A (ViewModel survives via stable contentKey)
  first render β†’ HomeScreen(route = "tab_one")
  re-delivery β†’ HomeScreen(route = "tab_two")
In the Activity case,
onNewIntent
is a callback naturally event-shaped, consumed and gone. In the Compose case, if I couple the param to the route
data class
, it lingers on the backStack entry forever after I've acted on it β€” the route now carries a stale instruction. Any later
recomposition
(config change, a sibling backStack mutation, anything that replays the route into the screen) either re-applies the stale command or forces me to remember to clear it explicitly. A channel above the entry lets me consume each re-delivery as an event instead of persisting it on the route.
Copy code
note: this is for my setup in particular where I have multiple NavDisplays and one of the NavDisplay having multi-backstack handling.
i
Yeah, this is what is called an XY Problem: https://en.wikipedia.org/wiki/XY_problem Where the root cause of your issue is actually another choice you made earlier (multiple NavDisplays)
πŸ‘ 1
So the correct fix is actually to just never get yourself into the situation you find yourself in - fill in the hole you dig for yourself and everything, including the problem you started this thread on, will have much simpler solutions
p
I wonder if the new ResultEventBus and ResulEffect APIs could help with your setup. Your usecase, as you described, is basically propagating an event from a root navdisplay down to some child navdisplay. I think the ResultEventBus could fit, the whole purpose of it, is to facilitate NavEntry intercommunication
k
ResultEventBus
fits well for my current setup and use-case. As for moving to a single
NavDisplay
given the scoped overlays, bottomsheet's and per-bottomNav-tab back stacks, it may introduce more complexity than it solves for my app specifically, have to explore more before moving to single
NavDisplay
.
πŸ‘ 1