One Off the Slack: State, Events, and Navigation

Tiago Nunes asked:

Hey everyone, how do I send a one-time event from the viewmodel to the composable? I want to navigate after a long operation

Part of the challenge is being “in the wrong frame” in the question.

In every UI system that I have ever seen, in the end, “what screen am I on?” is part of state, and events that change what screen that is showing creates a change in state. The problem is that too many UI systems hide that state and offer an API that is rather event-centric. This includes Android’s activity-and-fragments system, where the API is expressed in terms of events (“start an activity”, “replace a fragment”, “navigate to a destination”). So, we wind up thinking in terms of events affecting some “black box”, and we wind up tying ourselves in knots trying to figure out how to ensure that we are idempotent and that our event winds up changing our state correctly.

As the thread evolved, Tiago expressed concern about composables knowing stuff about navigation. Nelson Glauber countered:

My navigation logic is not in my composable 🙂 I keep it in the NavGraph. I just pass lambdas/callbacks to my composables. 😉

Google’s Manuel Vivo then chimed in:

Hi 👋 What are you using for navigation? Are you using the Navigation component? What about passing the navController as a dependency to the ViewModel so that the VM can handle the event instead of sending it to the composable?

…you can make the ViewModel be the source of truth for the navController instead of a composable. There’s nothing wrong with that

We’re coming up with guidance for events in Android. But it’s taking longer than expected. So far, the rules of thumb we have are:

  1. Let the ViewModel handle the event whenever it’s possible, and
  2. If the event is critical for the screen, model it as state

With Compose, #1 is possible because the Navigation component hoists its state via navController. This is something that’s not possible with the View system where the navigation state is a black box inside the NavHost itself

If we’re talking about Compose specifically, I think it’s safe to say that the state holder (that could be a ViewModel) should process the events and make state changes to reflect that on the UI.

i.e. do you need to show a snackbar? The state holder can take be the [source of truth] for the snackbarHostState, and call showSnackbar when needed. The same pattern applies for navController.navigate calls.

Colton Idle asked:

Do you think any of the compose sample apps do a good job at showing how navigation should be done in a pure compose app? I’m still hung up on how an app with a login screen would work + de authenciation would kick them out of the app.

Manuel replied:

You have to bear with us 🙂 this is also very new to us and we’re experimenting. In fact, I think there are only a couple of samples that use Compose Navigation. So I agree we need to cover more ground here

Colton then fretted a bit:

Passing navController to VM? I wonder if that keeps the AAC VM testable. Makes sense at a high level to me. But boy oh boy. Do we need more docs around this IMO.

Manuel was less worried:

Yeah, the VM is also testable because it takes the other hoisted states (e.g. navController and snackbarHostState) as dependencies so you can assert interactions with them and even fake them if needed

Part of the concern here is that Navigation for Compose is an Android-specific library, and not everyone is targeting Android alone, such as Tiago:

We’re also using KMM, but we’re not sharing the VM (yet). If we share the VM at some point, we might need to find another solution (because there might not be a way to supply the VM with a “navController” in the iOS side). In this use case, having a “dumb” VM sounds best. Of course this is out of scope, but still something to consider 🙂

Manuel agreed:

Ideally in iOS, you should also be able to hoist the navigation state. If that’s the case, you could hide the differences between the Android and iOS implementations under an interface, and let your state holder take that instead

I’m purposely using state holder instead of VMs because the latter is an implementation detail IMO

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!