https://kotlinlang.org logo
#compose
Title
# compose
s

spierce7

12/02/2023, 7:34 PM
I find it annoying that I navigate between screens in the
Composable
, but I'm doing all my business logic in the
ViewModel
. I'm curious, what do people do to drive screen changes from the
ViewModel
, or is even the goal of doing this wrong?
👀 1
z

Zach Klippenstein (he/him) [MOD]

12/03/2023, 2:41 AM
If your composable is your view layer, yea. In libraries like Square Workflow and other similar ones, the navigation is effectively “business logic” and entirely in that layer (or at least the libraries nudge you towards that architecture). I find that much simpler to reason about, and is a big reason why I dislike AAC ViewModels.
s

spierce7

12/03/2023, 2:46 AM
Do you have something you can link me as an example?
e

efemoney

12/04/2023, 10:26 AM
Navigation should always be in the business logic layer (my personal opinion). @spierce7 You will have to do some indirection. Create:
Copy code
// marker interface
// eg subclasses could be DeepLink(uri), PopBackStack, Route(string)
interface Direction or Command

interface Navigator {
  fun navigate(direction: Direction)
}
For the implementation pass those navigation commands into a channel. I chose a plain channel cos it models navigation events better (emissions are handled once)
Copy code
internal class RealNavigator : Navigator {
  val channel = Channel<Direction>()

  override fun navigate(direction: Direction) {
    channel.trySend(direction)
  }
}
In your activity or root of your nav graph, collect these “events” and handle them
Copy code
@Composable
internal fun CollectFromNavigator(
  navController: NavController,
  navigator: RealNavigator,
) {
  LaunchedEffect(navController, navigator) {
    navigator.channel.receiveAsFlow().collect(navController::handle)
  }
}
You can now inject a Navigator into your VM|Presenter and call navigate from business layer
s

Stylianos Gakis

12/04/2023, 4:49 PM
Gotta be careful not to have the navigator passed into the VM be a memory leak, since the navigator would be shorter-living that the VM itself, which typically outlives the UI itself, where that navigator would be instantiated. Also with such a channel you’d have to be ready for cases where such an event might come at the same time as a back event or something like that, which might mean that as you go from
A > B
to
A
(by popping backstack), you might also then consume an event to go to
C
and end up with
A > C
even though you might have wanted to cancel the move to
C
if you are already popping. This entire thing can get tricky, and while I totally see navigation as business logic too, I’ve had much better luck just modeling such situations as state, and handling them by reporting back to the VM that this “nav event” is handled and navigating, as opposed to “fire and forget” navigation events.
👍🏾 1
s

spierce7

12/04/2023, 4:52 PM
I’ve had much better luck just modeling such situations as state
This is how I've handled things with decompose, but I've found it to be extremely verbose.
e

efemoney

12/04/2023, 4:52 PM
How would the navigator cause a memory leak here? It doesnt hold any references to any shorter living objects. The entire point here is that the navigaor in this caes has the sae lifecycle (or higher) than the VM 🙂
s

Stylianos Gakis

12/04/2023, 4:53 PM
Right, this one in particular would not cause a leak, since it only holds a channel. So this one would only potentially be giving you the issue of the “global handler, not considering the current app’s state before handling the event” problem.
This is how I’ve handled things with decompose, but I’ve found it to be extremely verbose.
Verbose is one way to put it. Reliable is another way to put it 😄
1
e

efemoney

12/04/2023, 4:53 PM
I’ve had much better luck just modeling such situations as state
I would say avoid this at all if possible. Nav events are not “state”.
> you might also then consume an event to go to
C
and end up with
A > C
even though you might have wanted to cancel the move to
C
if you are already popping. This is not a problem of this approach 🤔 , in fact it has nothing to do with this and everything to do with nav library if this is allowed. Channels are internally synchronized afaik
s

Stylianos Gakis

12/04/2023, 4:57 PM
How is it not a problem? • You do some event on your VM • While it’s being processed you press back • While the UI is still animating backwards (the VM is still alive at this point) that processing is done and the event is shot, and your global handler consumes it and send that event to the NavController • At this point, as the NavController is navigating back to
A
, it then also navigates to
C
• You’re now at
A > C
e

efemoney

12/04/2023, 5:00 PM
Hmm for some reason I thought you meant a multithreading issue. Yes youre right 👍🏾 this is possible. It can probably be solved by • Not buffering nav events (or buffering in certain situations eg on rotation) • Reducing the scope of the event publisher from app-scoped to screen-scoped
s

Stylianos Gakis

12/04/2023, 5:02 PM
Yeah sorry, I didn’t explain it well enough before, not a multithreading issue. What do you mean by
Not buffering nav events (or buffering in certain situations eg on rotation)
?
e

efemoney

12/04/2023, 5:18 PM
Actually, I think know why I haven’t run into this issue. Our “VMs” are destination-scoped so are alive as long as the destination is in the back stack which is also the scope of the “long running operations” that would have ended in a nav event. So in your example, navigating back from that destination will actually destroy the destination-scoped VM & cancel its running operation meaning, ideally, no nav event emission. For your question I meant, that If you dont buffer the events, then a nav event can only be handled if there is at least one receiver/collector. So for the case where the screen is leaving (due to a back press) its receiver coroutine should be cancelled also and nav events pushed within that transition between both screens would be dropped. Just now realising your scenario extends to when the event is pushed after we have already landed on
A
, so yeah that wont really work.
s

Stylianos Gakis

12/04/2023, 5:24 PM
Yeah in general this is non-trivial, and the original suggestion you gave won't really work because of the issues you yourself mentioned too. Scoping this event handling to smaller components potentially solves some of it, but it's still in general non-trivial. And while I get everything you're saying, in my experience modeling nav as state, aka if the state is in X position, "clear that state + navigate to Y", while verbose and ugly looking, is very reliable and easier to reason about what happens in edge cases. Again, not a silver bullet and I'd love to have a "just do X and it all works perfectly fine" suggestion, but I unfortunately do not.
8 Views