If I have a button that when it is pressed it has ...
# compose
b
If I have a button that when it is pressed it has a chance to navigate, but needs to validate some other things on the form such as LocationTextField.isValid etc. Does it make sense to move from onClickButton = { navigateLambdaThatCallsNavController.navigate() } to
Copy code
val navigateToItemPickerList by viewModel.navigateToItemPickerList.collectAsStateWithLifecycle()

(this is passed into the ui further) onClickAddItem = viewModel::onClickAddItem,

LaunchedEffect(navigateToItemPickerList) {
    if (navigateToItemPickerList) {
        viewModel.resetNavigateToItemPickerList()
        onClickAddItem(locationSelectorUiState.selectedLocation)
    }
}
So that way I can perform all the validation stuff, and update the ui for the elements that failed validation, or if everything passes I can set the flag to navigate, and then this LaunchedEffect will trigger? Is there anything wrong with how I'm doing this, it's a pattern I follow in a lot of places and seems to work just fine, but wondering if there's a better way to do this sort of thing?
v
Im in no means an expert on compose and Android ViewModels (I mostly use Compose Multiplatform), but I prefer not to use State for events. Gotten bit by States caching values and creating hard to find bugs. Instead I use Channels. Then I can fire an event in my ViewModel and collect in a LaunchedEffect and do whatever.
Copy code
// HomeScreenViewModel    
protected val eventsChannel = Channel<E>()
    val events = eventsChannel.receiveAsFlow() // expose as flow
Copy code
// HomeScreen        

LaunchedEffect(Unit) {
            screenmodel.events.collect {
                log.d { "Event: $it" }
                when (it) {
                    is Event.LoggedOut -> {
                        navigator.replace(LoginScreen())
                    }

                    is Event.OnboardingComplete -> navigator.push(IntroScreen())
                }
            }
        }
    }
Bonus points for using a sealed interface to define Events
Copy code
sealed interface Event {
    data class OnboardingComplete(val user: String) : Event
    data object LoggedOut : Event
}
s
This article https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95 explains what the risks are with doing what you say Viktor. What you're doing is just fine Bryan. We do the same too. You're pretty much doing what is described here https://developer.android.com/topic/architecture/ui-layer/events#navigation-events-destination-back-stack and yes this is fine
☝️ 1
🙌 1
v
Thank you for sharing, @Stylianos Gakis . 🙏🏻 I think this is the part that I was missing. If I understand LaunchedEffect correctly, this makes the navigation only happen once even if the user goes back to the "Payment Complete" screen in the example?
Copy code
uiState.paymentResult?.let {
        val currentOnPaymentMade by rememberUpdatedState(onPaymentMade)
        LaunchedEffect(uiState) {
            // Tell the caller composable that the payment was made.
            // the parent composable will act accordingly.
            currentOnPaymentMade(
                uiState.paymentResult.paymentModel, 
                uiState.paymentResult.isPaymentSuccessful
            )
        }
    }
I need to sit with this a bit to make up my mind about it. I understand the reasoning around keeping everything in the UI state, but having to clear transient state (as mentioned as a Note at the end of the article) is also very annoying. Bugs from not clearing transient state is exactly what made me start using Channels instead. =)
s
Yeah you gotta remember that your VM does not get destroyed when you navigate somewhere. It instead stays in memory, so if you just go back the state might still normally be in the "navigate there now" and you end up in a loop if you don't clear the state. This however makes it so that the state won't get consumed before the navigation actually happens, so you ensure there's not "event" drops in some scenarios
👍 1
b
great discussion and thanks for the info Stylianos 🙂