Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    Hey! I'm trying to send some events from any object (ViewModel, for example) to a Composable function. To do this I've though about using
    SharedFlow
    instead of
    StateFlow
    because this should be a hot observable. I'm trying to collect the Flow in the Composable with
    val state = flow.collectAsState(defaultValue)
    even if this feels weird because it's not a state. • The Composable is not being recomposed when a new event is send, how's that achieved? • Is there any better way to consume/collect/observe a SharedFlow in a Composable function?
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    Events aren’t state, so collecting an event flow with
    collectAsState
    is, as you’ve called out, is wrong. You can use a
    LaunchedEffect
    and collect inside that
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    Oh, man. Thanks!! This should definitely be somewhere in the official docs.
    Colton Idle

    Colton Idle

    1 year ago
    Another way to think about it (as Adam Powell puts it) "A producer that outlives its consumer(s) should publish state. A producer whose consumer outlives it should publish events." So typically, your VM outlives its consumer (the composable) and so you might be better off representing an "event" as state.
    In general though... I am also really looking forward to more guidance around this sort of stuff. Hoping that when 1.0 drops, there ends up being a huge amount of guidance/docs around it.
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    I don't get why it should publish state. If the event is not consumed, then it should just be lost. Why persist that into state? In the Redux-web environment it's well known that you should get rid of time/temporality in state because it's not well modeled.
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    It is unusual to send custom events to compose – composables should effectively be functions that map your app state to UI. Events usually come from composables. That’s not to say there aren’t some use cases, but i think that explains why there’s such a convenient
    collectAsState
    function, but it’s a little more boilerplate to collect events.
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    I agree, but there are certainly some cases such as navigation or feedback messages that are undoubtedly events. And usually these events are sent by the ViewModel.
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    In the case of navigation, it is possible to express the current location in the app as state from a higher level of your architecture (or a different component, e.g. jetpack nav), so the navigation events are processed outside of your app’s composables. I’m not sure what “feedback messages” are.
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    Yes, you're right about navigation. I didn't think of it in that way. I will change that. With feedback messages I'm talking about snackbars, toasts, and that kind of temporal views.
    Colton Idle

    Colton Idle

    1 year ago
    Yeah. Navigation is basically the thing that I want specific guidance around. "there are certainly some cases such as navigation" I just "gave" into representing these things as state, and everything just works really nicely now. I was convinced by adams statement that I posted originally. Since the VM outlives the consumer (e.g. composable) then I should just use state. Which basically makes events something that I never need to use from a VM. /shruggie I really do think that we'll start seeing a few solid patterns once compose hits 1.0 since it'll really open up the adoption and community patterns will start to appear.
    Landry Norris

    Landry Norris

    1 year ago
    The way I handled Navigation (MultiPlatform Project with Nav in shared code, so that affected my decisions) was using a NavigationHelper that held a static String for each composable you could navigate to. My Single Activity had a NavigationComposable, which I defined, as it's content, and the NavigationComposable just observed a string in the NavigationHelper and called navController.navigate(string) when the String changed (iOS has the ability to observe a String for navigation too). This allows for Navigation in shared code by calling NavHelper.navigateTo(NavHelper.DASHBOARD)
    Halil Ozercan

    Halil Ozercan

    1 year ago
    feedback messages that are undoubtedly events
    you are partially right that feedback messages are kind of events but let's take a closer look at that idea from UI perspective. For example a backend request fails and you want to show an error message in UI. The verb "show" is hinting that this might be an event that you send to your UI from your ViewModel. However, Snackbar is a UI element. It has its own state and what you are actually doing is manipulating that state from outside. Navigation also works in a very similar fashion. There are tons of different ways and places you update a state. These "event"s are just one of those. When snackbar state receives a request to show a message, it simply updates the visibility and the text. After, it starts a timeout for reverting the visibility. Animatables, snackbar, navigation these are all stateful but not a great fit for ViewModel. So, the actual question is where do you draw the line? Which states should be completely owned by Compose and which should be outsourced to a ViewModel so that side effects can work. I don't have a clear answer for that. Thanks for coming to my non-sense TED talk...
    Chuck Stein

    Chuck Stein

    1 year ago
    I recently spent some time investigating how composables should handle events that come from the VM (as opposed to state). I agree with @Colton Idle in really hoping for more official guidance on this, as I was surprised such a common pattern (exposing one flow/observable from the VM for state and another for events) seems to have very little discussion around it in the compose world. I ended up adapting the pattern outlined in this article to work with compose, by calling a custom composable called
    EventHandler
    from my top-level screen composables. It's worked well for me so far. Hopefully this helps some people out who are in similar situations.
    /**
     * Collects a flow of instantaneous UI events coming from a ViewModel, so that the UI can react accordingly,
     * for example by navigating to a new screen or showing a snackbar.
     *
     * @param uiEvents The [Flow] of events to collect (each should represent a one-time event, not a persistent UI state).
     * @param eventCollector The function that will process each event.
     */
    @Composable
    fun <T> EventHandler(uiEvents: Flow<T>, eventCollector: suspend (T) -> Unit) {
        val lifecycleOwner = LocalLifecycleOwner.current
    
        val uiEventsLifecycleAware = remember(uiEvents, lifecycleOwner) {
            uiEvents.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        }
    
        LaunchedEffect(uiEventsLifecycleAware, eventCollector) {
            uiEventsLifecycleAware.collect(eventCollector)
        }
    }
    For the type parameter representing events,
    T
    , I use an enum containing the possible events for that screen, or if some events contain data, a sealed class instead.
    I disagree with some in this thread suggesting that events should just be represented as part of the composable's state. If we set the state to something that causes navigation or a snackbar, then every time we recompose we will reshow the snackbar, or if we pop the screen off the back stack after navigating, we will immediately navigate again, causing the user to get stuck in a loop. Unless, of course, the composable calls a VM function to reset the state after handling these updates. But this is more boilerplate and error prone since one could easily forget to call this function. Why should the common pattern of exposing state and events separately from the VM be any different in the compose world?
    Colton Idle

    Colton Idle

    1 year ago
    "I disagree with some in this thread suggesting that events should just be represented as part of the composable's state. If we set the state to something that causes navigation or a snackbar, then every time we recompose we will reshow the snackbar, or if we pop the screen off the back stack after navigating, we will immediately navigate again, causing the user to get stuck in a loop. Unless, of course, the composable calls a VM function to reset the state after handling these updates." @Adam Powell sorry for the ping (especially on the eve of a big day ahead) but any thoughts here? It goes directly into the topics we were talking about the other day.
    Adam Powell

    Adam Powell

    1 year ago
    the events shouldn't be represented as state, they should be handled, yielding state, and compose should act on that state. Examples of this would be things like (Event fired: show snackbar, "Hello, world!") => (Event handler callback: append "Hello, world!" onto snack queue) => (State: Snack queue:
    ["Hello, world!"]
    )
    Colton Idle

    Colton Idle

    1 year ago
    "yielding" state. Big oof from me. Right when I thought I got it. 😅 So my "loggedInState" here would wrong? https://github.com/ColtonIdle/ComposeSignInSample/commit/adef3b2897d794a9e20cc96ca0fa09acb891148e
    Adam Powell

    Adam Powell

    1 year ago
    you're fine, your
    signIn
    method setting it to true is exactly what I mean here
    Alex Vanyo

    Alex Vanyo

    1 year ago
    Towards the boilerplate comment: For the very simplest cases, there is a tradeoff that "the system" isn't handling quite as much for you. However, in exchange you get a reward: you have to fight the system less for more complicated cases. For an example, a
    Dialog
    in the view system has a fairly straightforward
    show()
    method at first glance. However, if you want to hide the dialog manually, you have to keep track of it. If you are not careful, you might show two dialogs. What happens if your app is rotated, or goes into the background and goes through process death. Do you still want your dialog to be shown? If you can avoid trying to pass a ("Show a dialog: "Hello, world"") event down into the UI, and instead treat it as a ("Currently showing dialog: "Hello, world"") state, you now have a lot more control over what the user will see.
    Colton Idle

    Colton Idle

    1 year ago
    Ah. Adam I think we're talking about events in two different ways again. So you're saying that events should "yield" state. Like my onClick signInButton "event" should yield some kind of state (i.e. loggedInState is updated) But where me and Chuck are saying "event" is some kind of observable that is exposed from the VM. So it's like a different direction of an event being sent entirely. The only reason I have this concept of "events" being emitted from the VM is because in the Fragment + VM world, that's what all of my teams used to emit an event from VM to Fragment (think toast, snackbar, navigation)
    @Alex Vanyo yeah. I think it's just trying to break out of the mold that I've been in with Fragments + VMs in the past (^^^ toast, snackbar, navigation). Now after talking to people in this channel, I think I rather just have things like toast, snackbar, and navigation just represented as state.
    Chuck Stein

    Chuck Stein

    1 year ago
    @Alex Vanyo Could you explain a bit more what the disadvantage is with collecting an event flow separately from state? How would we end up showing the dialog twice? From my understanding the whole goal with a separate event stream (e.g.
    SingleLiveEvent
    or a Kotlin
    Channel
    ) is so that the response to the event only happens once, e.g. don't pop up a snackbar a second time after rotating or on recomposition. Also, what is it about the compose paradigm that makes this event stream pattern obsolete?
    Alex Vanyo

    Alex Vanyo

    1 year ago
    I should clarify that in the Compose world, you'd want to be using the Compose version of
    Dialog
    , rather than the non-compose
    AlertDialog
    or variants. That composable
    Dialog
    is much more conducive to being controlled by state, rather than needing an explicit show()/hide() call. That's the bit that allows you to avoid needing an event stream to the UI:
    val openDialog = remember { mutableStateOf(true) }
    if (openDialog.value) {
        Dialog(onDismissRequest = { openDialog.value = false }) {
            // Draw a rectangle shape with rounded corners inside the dialog
            Box(Modifier.size(200.dp, 50.dp).background(Color.White))
        }
    }
    For showing the dialog twice, I was imagining a case something like this: Suppose you want to show some sort of error dialog upon a network request failing. If a rotation causes the request to kick off again, (or maybe the user backgrounds the app, and the process is killed, and upon returning to the app the request is made again), or there's any other way for another dialog to be shown, you might get two dialogs visible at the same time if the user hadn't dismissed the first one yet. Each dialog might only have been shown once, but it gets a lot more complicated to choreograph the behavior of both/all of them.
    Chuck Stein

    Chuck Stein

    1 year ago
    Hmm, I would still show/hide the dialog in the UI the same way as your snippet above, using
    openDialog
    . But my point is that rather than having the
    ViewModel
    expose a
    State
    with a
    showDialog
    boolean set to
    true
    , it could expose something like
    Flow<Event>
    where
    Event
    is an enum for all the possible events on that screen, including
    SHOW_DIALOG
    .
    openDialog
    would be initialized to
    false
    , but then if the
    SHOW_DIALOG
    event is emitted, we set
    openDialog
    to
    true
    . That's the only difference, and it's not possible for the UI to ever show more than one dialog because the composable can only ever either emit that one
    Dialog
    if
    openDialog
    is
    true
    , or not if it's
    false
    . And with this event stream approach rather than exposing it as
    State
    from the
    ViewModel
    , we wouldn't need to add a function like
    onDismissDialog
    to the
    ViewModel
    to set the
    showDialog
    State
    back to
    false
    , and call it in the Dialog's
    onDismissRequest
    callback.
    Colton Idle

    Colton Idle

    1 year ago
    I think to prevent "things" from being called twice, that's why you react to these states with "LaunchedEffect" or else you could have things happen 10000 times because recomposition is out of your control.
    Chuck Stein

    Chuck Stein

    1 year ago
    But if it exits the composition then re-enters, and it's still reading from the same
    ViewModel
    exposing the same
    State
    as when you left the screen, which includes something like
    navigateToNextScreen = true
    , then that
    LaunchedEffect
    block will run again and immediately navigate to the next screen once again, then the user is stuck in that loop I mentioned before. I do use
    LaunchedEffect
    for collecting that
    Flow<Event>
    from the
    ViewModel
    , but using
    State
    for something that should only happen once just causes complications.
    Colton Idle

    Colton Idle

    1 year ago
    Hm. I've basically been doing ScreenA with button that says "next", onClick of next I set a "allDataValidated" state = true ScreenA then has an if state if allDataValidated then LaunchedEffect{ navController.navigate("ScreenB") } Then ScreenB loads on the screen, and if I hit back, then ScreenA shows, and I'm not redirected to ScreenB until I hit the next button again It all seems to work without any duplication? idky tho. When I spell it out like that... I guess I would assume that it would happen again?
    Actually. Shit. I'm wrong. It does repeat the action on me (i.e. navigating to ScreenB). Whelp. I have no idea how I thought this was working before. lmao
    To "fix" it for now, I'm going to do this.
    LaunchedEffect{
    navController.navigate("ScreenB")
    vm.allDataValidated = false
    }
    Besides that... without more official guidance... I'm kinda lost. lol @Chuck Stein you were right.
    Chuck Stein

    Chuck Stein

    1 year ago
    That's what I mean, if you use state then you have to revert the state back to the previous value after handling it, whereas this really indicates it should be an instantaneous event rather than state. I would recommend you read the medium article and code snippet both in my earlier message if you want an approach that guarantees each event is only handled once, and doesn't require resetting a state value each time you handle it. @Colton Idle
    Colton Idle

    Colton Idle

    1 year ago
    I'm weirdly not opposed to resetting the state every time because... it at least makes sense to me vs all of these other flow vs stateflow vs livedata vs hot flowable vs cold and everything in between. I need a phd to understand that stuff apparently, and just resetting the state will work for now... until some docs with simple guidance are available. lol
    Chuck Stein

    Chuck Stein

    1 year ago
    Fair enough, official docs on this would be nice 👍
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    One disadvantage I can see of representing the events in the state and resetting them when handled is if you want more than one component to react to those events. This is exactly one of the advantages of using
    SharedFlow
    , isn’t it? You just want more than one observer to react to a single time event. If that event is present in the state, the first observer would be the one resetting the state, and what about the others? I mean, it’s probably something you can make work, but I don’t really see the downfall of just sending single time events. This is React/Redux, but

    this

    video has an interesting point of view about removing time from state. Web dev is some years ahead in declarative UIs, so we can learn some things from them, maybe.
    Adam Powell

    Adam Powell

    1 year ago
    bunch of things getting conflated here 🙂
    1) Events are not state; don't model them as state. Where I've recommended modeling state instead of events, implied in that statement is rethinking the nature of what is being modeled, and modeling the output of handling those events as state, not storing the events in flight to be handled later.
    2) w.r.t. "what's different about compose than before?" Compose changes the landscape in two ways (and yes it's very analogous to declarative web systems as well) - components you have to interact with tend not to own their own state, and terser code makes it easier to see the data flow.
    When you're working with components that own their state, it's hard to interact with them any other way than sending requests to them (events) and observing changes to their state externally. You don't have many other options.
    This naturally shapes code around an event dispatching mindset.
    When those components observe external state instead, that design constraint no longer exists and other options are open.
    We've recommended a state down/events up unidirectional data flow pattern in most of the compose materials. The reason why is because it maps closely to the lifetime of components involved; something "down" hierarchically is a descendant created by the parent with an overall lifetime owned and controlled by the parent. Something "up" is an ancestor generally with a longer lifetime.
    Landry Norris

    Landry Norris

    1 year ago
    Sorry if I'm missing something, but to check my understanding from this thread: the old way of thinking about UI worked well with an event based approach, but the 'new' way of compose does away with events, preferring external state, which is presumably handled in a ViewModel, and away from the composable, does this sound right?
    Adam Powell

    Adam Powell

    1 year ago
    it doesn't do away with events, but it removes some of the pressure for events to do everything.
    State and event each have some relevant characteristics for this relationship of whether producer outlives consumer or consumer outlives producer. State is. It's a declaration of fact. Observing state doesn't change state; it's always safe for state to have zero observers or 100 observers.
    It's also irrelevant what a previous state used to be. Knowing current state is always enough for an observer to do its job.
    That makes it a good fit for when a consumer lives a shorter lifespan than the producer; a newly created consumer never has to play catch-up with old events it missed or somehow reconstruct an initial state.
    By contrast, events are time sensitive. Events happen and then they're over, the only evidence that they were ever there is the output state produced by handling them.
    Events often (but not always) form stateful protocols. For example, a touch release event only makes sense if it follows a press event. This means that it's not a given that a new consumer can arrive midway through a stream of events and be able to make logical sense of it.
    Event handlers are often reducers for state. These join points between entities turn these time-based events into state that can be consumed by any number of entities in the rest of the system.
    With this mental model a component that owns its own state is its own reducer; method calls on an object represent events in time, and those events are reduced by the implementations of those methods into the object's internal state.
    An event handler/consumer that outlives the event producer can always see every event produced end to end; there's never a question of whether any events were lost as part of a stateful protocol.
    There's also no diffusion of responsibility; exactly one consumer is responsible for handling the events and it is present the whole time the producer exists. There's no handoff of responsibility to coordinate.
    All question of how to store events until a consumer becomes present go away. Concerns of whether the right consumer in the right state received an event similarly vanish.
    (And if you've read the kotlinx.coroutines discussions around channels and atomic cancellation, you know that this topic is deeper and has more edge cases than it looks like at first 🙂 )
    If you hoist state up to the point in your hierarchy where you can handle events flowing "up" immediately and apply the results to state owned at that point, you remove a whole host of edge cases that would otherwise exist.
    Colton Idle

    Colton Idle

    1 year ago
    Adam Powell

    Adam Powell

    1 year ago
    why not navigate to the next screen when the next button is clicked?
    Colton Idle

    Colton Idle

    1 year ago
    Because there's some sort of thing (validation, business rules, etc) that has to happen. Example, So on button press, if your membershipType is a certain level, then you pass onto the next screen. If your membershipType is lower than that, then you see a Toast "not allowed to do this" How would that example work?
    Chuck Stein

    Chuck Stein

    1 year ago
    ^ Exactly, and it could be something like validation from the backend failing. So there will always be cases where some events come from the ViewModel rather than from the UI hierarchy, breaking that unidirectional data flow. In that case it makes sense for the VM to emit an event rather than State, right Adam? Through a Channel, for example.
    Adam Powell

    Adam Powell

    1 year ago
    No, I don't think that makes sense as opposed to representing the results of processing those events as state in the VM instead. You can do it with events, but you take on additional complications and make unnecessary work for yourself by doing it that way.
    Chuck Stein

    Chuck Stein

    1 year ago
    What are those additional complications and extra work? I see additional complications and extra work with representing the event as State, because the UI has to call a ViewModel method to reset the temporal state after handling the event (by showing a snackbar or navigating or whatever it may be).
    Adam Powell

    Adam Powell

    1 year ago
    The event isn't represented as state, that's the whole point. 🙂 Your UI is a function of app state; app state in, UI out. You can test or verify any state of the app by supplying a complete input state. If you have to poke a series of events into your UI components to cause them to achieve a particular state on your behalf, that's more complicated.
    Landry Norris

    Landry Norris

    1 year ago
    In theory, the ViewModel should handle temporal state itself, right? The UI only needs to care about direct interactions with views, such as button presses and textfield changes. It gets the rest from the ViewModel as a state.
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    Everything makes sense, but I'm still not seeing how would you make the Composable show a Toast when something happens on the ViewModel. Something as basic as this. If it should be included in the state with something like toastVisible : Boolean then something must set it back to false. Is this the desired approach? I find it more error prone and hard to manage than simply let the ViewModel expose a Channel/SharedFlow/SingleLiveEvent. If that's not what you're saying, then I'm definitely not getting something.
    Colton Idle

    Colton Idle

    1 year ago
    @ Adam, soo... from what I gather you're saying something like this is fairly legit?
    @Composable
    fun ScreenA(goToScreenB: () -> Unit, vm = hiltViewModel()) {
      if (vm.membershipValidated){
        LaunchedEffect{
          someLambda() //ends up calling navController.navigate("ScreenB")
          vm.membershipValidated = false //acknowledge the state and reset it
        }
      }
    
      Column() { //actual screen content}
    }
    Landry Norris

    Landry Norris

    1 year ago
    A LaunchedEffect uses the viewModel a couroutine scope internally as the mechanism if I remember correctly. Shouldn't the ViewModel handle all of that logic itself? For example, something in the vm sets membershipValidated to true. Can't you just call viewModelScope.launch inside the vm to launch someLambda instead? From my limited understanding of compose, the goal is to move as much out of the view as possible. Sorry if I don't fully understand compose. I just want to check that I understand the relationship between Views and ViewModels.
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    LaunchedEffect
    can also be used for view-only stuff that the view model shouldn’t know about, like running individual animations.
    Landry Norris

    Landry Norris

    1 year ago
    Yeah. It's good for affecting the view, and I've seen people use it to notify the viewModel to ask the repository to load data (I don't know how good/bad of a practice that is. Time will tell), but the fact that the state is so tied to it in the previous example indicates that the vm should probably handle the situation Colton is referencing.
    Adam Powell

    Adam Powell

    1 year ago
    a useful way to think of LaunchedEffect is as an actor. You're declaring the presence of an entity that is going to do a thing.
    I would avoid that kind of flip-flop construct written above with vm.membershipValidated; there are several things I'd question about it:
    1. who owns it? It appears to be mutable from anywhere, so how can its value be trusted as a source of truth? 2. the membership is no less validated just because that effect ran, why is the effect changing state to say so?
    @Nacho Ruiz Martin there are a lot of reasons why I would look at
    Toast
    as a special case and not something to derive best practices from. 🙂 Its problems as a fire and forget event sent to a
    Context
    run very deep and the android WindowManager team has been working to mitigate the side effects from that for years. Some of these are also reasons why Snackbars are the preferred way to display that sort of information.
    Colton Idle

    Colton Idle

    1 year ago
    1 and 2: Valid points it was just an example because I don't know how else I would do this example:
    Goal is to go from @Composable ScreenA to @Composable ScreenB after some logic is done on a button press.
    
    Real life example: So on button press, if your membershipType is a certain level, then you pass onto the next screen. If your membershipType is lower than that, then you see a Toast "not allowed to do this"
    
    How would that example work?
    I am literally just making stuff up because I don't know how I would do the example above which (to me) seems like a simple example. But I'm dead in the water. (I understand toast is a special case... but still there must be some way to trigger it "just" once, no?)
    Adam Powell

    Adam Powell

    1 year ago
    I think your problem statement translates quite literally:
    onClick = {
      if (membershipType >= requiredLevel) {
        navigateToNextScreen()
      } else {
        reportError()
      }
    }
    Colton Idle

    Colton Idle

    1 year ago
    This "business logic" should arguably in the VM... no?
    Landry Norris

    Landry Norris

    1 year ago
    I've used onClick = viewModel::handleButton, where viewModel#handleButton has logic like Adam wrote. Is this correct?
    Adam Powell

    Adam Powell

    1 year ago
    which part do you consider the business logic? You could pull part of that out if you felt so inclined:
    if (vm.hasValidMembership) { // ...
    @Landry Norris it can be, presuming that the viewModel actually has all of the context required to handle the event. It's the point where the VM is reflecting events back "down" to shorter-lived components that things start getting messy.
    If your navigation controller were held by your ViewModel for instance, sure, call navigate on it directly as part of that method. But if it's not, then you have different join points.
    Colton Idle

    Colton Idle

    1 year ago
    @Adam Powell yeah, I consider an "if" statement being business logic.
    Adam Powell

    Adam Powell

    1 year ago
    alright then,
    vm.validateMembership(
      onSuccess = navigateToNextScreen,
      onFailure = reportError
    )
    you can move these abstractions around and still preserve properties of the data flow
    Colton Idle

    Colton Idle

    1 year ago
    navigateToNextScreen
    and
    reportError
    would be lambdas passed into the composable right? i.e.
    vm.validateMembership(
      onSuccess = navigateToNextScreen(),
      onFailure = reportError()
    )
    Landry Norris

    Landry Norris

    1 year ago
    I'm currently working on a multiplatform project, so that affects the design I used, but I have a global NavigationHelper that you can call navigateTo(string) on from anywhere. The NavHosts register callbacks with this NavigationHelper to perform the actual navigation. This allows navigation to be done from anywhere in the code (and works well with the SwiftUI NavHost equivalent).
    Adam Powell

    Adam Powell

    1 year ago
    so long as you can get the state saving for that to work out, 👍
    that's the part that complicates situations like this on android: both AAC ViewModel and savedInstanceState essentially define a lifespan for a piece of data that outlasts the activity and everything in it (e.g. your composables) but that isn't lexically available at the root of the activity, only further down by a separate cross-cutting mechanism
    so instead of the, "state down, events up" model being one dimensional, there are more dimensions of lifetime and "down" vs. "up" becomes, "from longer to shorter lived entities" and "from shorter to longer lived entities" respectively
    Colton Idle

    Colton Idle

    1 year ago
    Thanks for the tips Adam. It might take longer to break into this new mindset than I thought. I will try to apply your approach above i.e.
    vm.validateMembership(
      onSuccess = navigateToNextScreen,
      onFailure = reportError
    )
    the next time I encounter this problem.
    Alex Vanyo

    Alex Vanyo

    1 year ago
    If
    navigateToNextScreen
    is a lambda that contains a reference to a
    navController
    or something like an
    Activity
    for
    startActivity
    , is there a concern now that a
    ViewModel
    might have a reference to a shorter lived component?
    Colton Idle

    Colton Idle

    1 year ago
    @Adam Powell updated my code to
    action = {
      vm.validateMembership(
        onSuccess = navigateToNextScreen,
        onFailure = reportError
      )
    }
    So simple. idk why it didn't click before. It works 😍
    Adam Powell

    Adam Powell

    1 year ago
    @Alex Vanyo I'm making the assumption that any async behavior would leverage suspend functions and structured concurrency, e.g.
    val scope = rememberCoroutineScope()
    // from compose-foundation, cancels old request in flight and ensures mutual exclusion
    val mutatorMutex = MutatorMutex()
    Button(
      onClick = {
        scope.launch {
          mutatorMutex.mutate {
            vm.validateMembership(...
    at which point the lifetimes of any lambda captures are controlled
    (which is to say, the lambdas pased to onSuccess/onFailure from that example are not retained after
    validateMembership
    returns or throws)
    Alex Vanyo

    Alex Vanyo

    1 year ago
    Gotcha, that should avoid leaks, although it is a bit delicate of a handshake. If I’m understanding it correctly, that setup will only work for an event that originates from the
    @Composable
    layer. If there’s an event that originates from the
    ViewModel
    layer or application layer, that needs to perform a “one-shot” update (navigate, call
    startActivity()
    ), then we’re back in the situation where it feels like we need an event to be passed from the
    ViewModel
    “down” to the UI
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    This is such an interesting debate, btw. Thank you all for it.
    Adam Powell

    Adam Powell

    1 year ago
    What I'm saying is that there doesn't have to be events that originate from the ViewModel or application layer to be consumed by the UI at all. The ViewModel offers state, and at the point in your app where both the ViewModel and your navigation state are available, you can set things up to act on that state if necessary.
    I don't get a, "login completed successfully" event, I observe
    vm.loginState is LoginState.LoggedIn
    Colton Idle

    Colton Idle

    1 year ago
    sooo I could essentially do
    if (vm.loginState is LoginState.LoggedIn) {
      LaunchedEffect(Unit) {
         someNavigationLambdaThatWasPassedIn()   
      }
    } else {
      Column(){ // my actual layout}
    }
    or
    Column(){ // my actual layout, with a buttonClickable
      buttonClick = {
        vm.loginUser(success = someNavigationLambdaThatWasPassedIn)
      }
    }
    Alex Vanyo

    Alex Vanyo

    1 year ago
    I think the last hurdle for me to believe “at all” is the way to guarantee that the “act on that state if necessary” happens exactly once, if that’s what the API calls for.
    Colton Idle

    Colton Idle

    1 year ago
    Can you rephrase that @Alex Vanyo. Not sure I follow?
    Alex Vanyo

    Alex Vanyo

    1 year ago
    For a concrete example, let’s say the requirement is show a
    Toast
    exactly once with the error message from the request that caused the user to sign out. If we yield that failed request into a state on the
    ViewModel
    side to only pass down state, then on the UI side it has to act upon that state to produce the
    Toast
    , and then “reset” the state to keep the “only once” requirement. (For the reasons above,
    Toast
    is not the example to follow for general best practices, but it is a simple case where it’s a fire and forget API)
    As Adam mentioned above, that resetting pattern is generally to be avoided, but for the “at all” to be true, I think it might be called for in some special cases that can’t be reformulated to act idempotently
    John O'Reilly

    John O'Reilly

    1 year ago
    Really interesting thread but unfortunately haven't been able to keep up with all the comments....can someone please create a blog post that summarises this 🙂
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    1 year ago
    I'd gladly do it but there isn't any conclusion yet. There are two confronted approaches: • Include events in state when it makes sense and use lambdas from view otherwise • Just send the event using a dedicated channel in the ViewModel Adam is giving strong arguments for the first one, but there are still some doubts about how to finally implement it. For example: If the event is represented in the state, the property should be reset to the "Idle" value as soon as it is consumed. Is this a good approach? Who's in charge of that? If the property is not reset, how do you prevent the event being consumed more than once? Also: Would there be any way to have more than one observer in that case?
    Shakil Karim

    Shakil Karim

    1 year ago
    Wow! soo much to learn from this Thread, @Adam Powell I really wish there will some Official Documentation around State vs Event.
    I have a similar use case like @Colton Idle mention here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1627494325428300?thread_ts=1627406028.253800&amp;cid=CJLTWPH7S But in my case, I think I can't avoid Flip flop pattern which @Adam Powell mention above because of pagerState is own by Composable, but I am not sure is this a correct Approach? data class QuizState( val isComplete: Boolean = false val quizList: List<String> = emptyList() ) class SomeViewModel : ViewModel() { val quizState = remember { mutableStateOf(QuizState()) } fun resetState() { quizState.value = quizState.copy(isComplete = false) } // Some external entity call this method fun markComplete() { quizState.value = quizState.copy(isComplete = true) } } @Composable fun SomeCompose(viewModel: ShowDetailsViewModel) { SomeCompose(viewModel.quizState) { viewModel.resetState() } } @Composable internal fun SomeCompose(quizState: QuizState,onReset: () -> Unit) { val pagerState = rememberPagerState(pageCount = quizState.quizList.size) LaunchedEffect(quizState.isComplete,pagerState) { if(quizState.isComplete) { pagerState.animateScrollToPage( pagerState.currentPage + 1) onReset() } } HorizontalPager( modifier = Modifier.fillMaxSize(), state = pagerState, dragEnabled = false ) { } }
    Adam Powell

    Adam Powell

    1 year ago
    I think this thread is getting pretty unwieldy at this point. 🙂 Request for some more docs and materials around this topic heard loud and clear.
    @Shakil Karim consider what your data modeling is saying; what does it mean for a quiz to be complete? Is the quiz any less complete from your data modeling perspective because you finished scrolling to a different page? Or should that be tracked as a different piece of state somewhere?
    Shakil Karim

    Shakil Karim

    1 year ago
    @Adam Powell Sorry for the confusing name,
    isComplete
    here is just an example, it basically represents some action that is true/false ( in my case there is a sound played for correct or incorrect answer and after that ViewModel's method
    markcomplete
    get called, which scroll the pager to the next Quiz page and reset
    isComplete
    ) I only want to scroll to the next page when the sound is finished playing.
    Colton Idle

    Colton Idle

    9 months ago
    Hey everyone. It's been some time since this thread (108 replies!!!), but there is official documentation on UI events you can read here: https://developer.android.com/jetpack/guide/ui-layer/events Happy holidays!
    Shakil Karim

    Shakil Karim

    9 months ago
    "ViewModel events—should always result in a UI state update" I think I will still use Channel to the prograde event to UI (Compose function).
    Nacho Ruiz Martin

    Nacho Ruiz Martin

    9 months ago
    Thanks for that, Colton! I still can’t see the benefit of holding those events in state if they are going to be immediately reset instead of having a dedicated channel in the form of single time events. But it’s still good to have some official doc there.
    Chuck Stein

    Chuck Stein

    9 months ago
    So the official recommendation is to use UI state for one-time events even though this means you have to reset them (bug prone)... Doing something and then needing to remember to always undo it, isn't this why garbage collection was invented? 😅
    Adam Powell

    Adam Powell

    9 months ago
    no, the official recommendation is to process one-time events immediately, yielding UI state as an output of processing the event (reducing the events.) The UI observes and reflects that state.
    Colton Idle

    Colton Idle

    8 months ago
    lol Developers_: we want official documentation on this use case!!_ Google_: Okay. Here ya go._ Developers_: Nah. Nevermind._ Just making a bit of fun at the sequence of events. 😄 I think its fine for people to use their preferred way. In my case, I don't want to have to think about these things and the corner cases that they introduce. Also. The docs state that "The problem with exposing events from the ViewModel is that it goes against the state-down-events-up principle of Unidirectional Data Flow." For those two reasons I'm going to use the way the docs suggest.
    Chuck Stein

    Chuck Stein

    8 months ago
    Don't get me wrong, I'm happy I know what's officially recommended now. If I face any issues with my current event channel approach then I will gladly switch over to it. I just find my current approach a bit simpler and less error prone because it doesn't require resetting the state each time, but I see the advantages of holding these events in the UI state so that it can be a single source of truth for how the UI should look. In my case the source of truth is the state + event channel