I am using Jetpack Compose, and am trying to commu...
# android
n
I am using Jetpack Compose, and am trying to communicate a navigation event from the view model back to the screen composable. The idea is that the user enters their username in a text field, hits "Next", and then the view model makes a call to an API to determine whether to use an internal login (in this case we navigate to a password entry screen), or use a web login (in this case we navigate to a screen hosting a WebView). My proof of concept is this:
Copy code
typealias NavEventListener<T> = (T) -> Unit

class NavEventDispatcher<T> {
    var listeners = mutableListOf<NavEventListener<T>>()

    fun dispatchEvent(value: T) {
        listeners.forEach {
            it(value)
        }
    }
}

@Composable
fun <T> HandleNavEvent(dispatcher: NavEventDispatcher<T>, listener: NavEventListener<T>) {
    DisposableEffect(dispatcher) {
        dispatcher.listeners.add(listener)
        onDispose {
            dispatcher.listeners.clear()
        }
    }
}
Basically, a screen composable calls
HandleNavEvent(viewModel.dispatcher) { eventPayload -> onNavigate(...) }
, and the view model simply adds a dispatcher instance. I have this dispatcher as a second public property of the view model alongside a single state flow describing the UI state. My question is, is there a better way to do this? It seems to work for the most part, I'm new to compose and am wondering if there is anything I'm not considering here. The motivation for this is my architecture has it such that, if we just navigate by watching the UI state, then when we navigate back, you bounce right back to the second screen because the UI state for the previous screen is still one which means "navigate forward". I needed a way to reset the state, and didn't want to have a requirement that all my UI states have a "reset" method which resets only part of it. I figured separating the UI state from the navigation state was the way to go.
h
If you just need one-time actions (events), you can use channels with flow
Copy code
private val _captureEvents = Channel<Unit>()
val captureEvents = _captureEvents.receiveAsFlow()
And collect flow in compose function
Copy code
LaunchedEffect(true) {
    viewModel.captureEvents.collect {
        showSnackbar(it)
    }
}
n
I'm struggling to implement this in a way that suits my use case. Channels like to buffer at least 1 element (event) at all times. So if events are sent while a view is off-screen, one of those events will get stacked and sent immediately when the composable comes back on the screen. Seems the only way to get a capacity of zero is to have a suspending channel, which effectively is a buffer.
Seems like a shared flow fits my use case here. Can be empty, indicating no events, can set the replay to zero, etc. Is this something I should package with my uiState object (and re-create one every time the uiState updates, so the ui only observes a single thing, rather than both a ui state flow plus a viewmodel shared flow), or am I correct in keeping a single shared flow here to represent all possible navigation events? I know compose likes you to collect things as States if possible..
h
It's called Redux architecture and mostly used in frontend apps. In Android it's called MVI and you can use it if you want. About channel and buffer. By default channel is suspended, so you can push event only when something consumes it. No problems with event repetition or not delivering
You can read about using channels and flows for events here https://link.medium.com/hVh8oTxYfnb
123 Views