Hey! I'm trying to send some events from any objec...
# compose
n
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?
🏅 1
z
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
n
Oh, man. Thanks!! This should definitely be somewhere in the official docs.
c
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.
☝️ 6
n
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.
👍 1
z
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.
☝️ 1
2
n
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.
z
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.
🤔 1
n
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.
c
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.
🤞 1
l
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)
👍 1
h
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...
🧸 1
💯 1
c
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.
Copy code
/**
 * 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?
👍 3
4
c
"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.
a
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!"]
)
c
"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
a
you're fine, your
signIn
method setting it to true is exactly what I mean here
a
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.
💯 2
👍 1
☝🏻 1
c
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.
👍 1
c
@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?
a
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:
Copy code
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.
c
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.
👍 1
c
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.
c
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.
c
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
😲 1
To "fix" it for now, I'm going to do this.
Copy code
LaunchedEffect{
navController.navigate("ScreenB")
vm.allDataValidated = false
}
Besides that... without more official guidance... I'm kinda lost. lol @Chuck Stein you were right.
c
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
1
c
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
c
Fair enough, official docs on this would be nice 👍
n
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.
a
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
👌 2
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.
l
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?
a
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 🙂 )
⏸️ 1
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.
c
a
why not navigate to the next screen when the next button is clicked?
c
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?
c
^ 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.
a
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.
c
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).
a
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.
l
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.
n
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.
4
☝️ 1
c
@ Adam, soo... from what I gather you're saying something like this is fairly legit?
Copy code
@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}
}
l
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.
z
LaunchedEffect
can also be used for view-only stuff that the view model shouldn’t know about, like running individual animations.
l
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.
a
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.
c
1 and 2: Valid points it was just an example because I don't know how else I would do this example:
Copy code
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?)
a
I think your problem statement translates quite literally:
Copy code
onClick = {
  if (membershipType >= requiredLevel) {
    navigateToNextScreen()
  } else {
    reportError()
  }
}
👍 2
c
This "business logic" should arguably in the VM... no?
👍 1
l
I've used onClick = viewModel::handleButton, where viewModel#handleButton has logic like Adam wrote. Is this correct?
a
which part do you consider the business logic? You could pull part of that out if you felt so inclined:
Copy code
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.
🤔 1
c
@Adam Powell yeah, I consider an "if" statement being business logic.
a
alright then,
Copy code
vm.validateMembership(
  onSuccess = navigateToNextScreen,
  onFailure = reportError
)
you can move these abstractions around and still preserve properties of the data flow
c
navigateToNextScreen
and
reportError
would be lambdas passed into the composable right? i.e.
Copy code
vm.validateMembership(
  onSuccess = navigateToNextScreen(),
  onFailure = reportError()
)
l
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).
a
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
c
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.
Copy code
vm.validateMembership(
  onSuccess = navigateToNextScreen,
  onFailure = reportError
)
the next time I encounter this problem.
a
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?
👍 1
c
@Adam Powell updated my code to
Copy code
action = {
  vm.validateMembership(
    onSuccess = navigateToNextScreen,
    onFailure = reportError
  )
}
So simple. idk why it didn't click before. It works 😍
🙌 2
a
@Alex Vanyo I'm making the assumption that any async behavior would leverage suspend functions and structured concurrency, e.g.
Copy code
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
👍 1
(which is to say, the lambdas pased to `onSuccess`/`onFailure` from that example are not retained after
validateMembership
returns or throws)
a
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
4
n
This is such an interesting debate, btw. Thank you all for it.
👍 2
a
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
👍 3
c
sooo I could essentially do
Copy code
if (vm.loginState is LoginState.LoggedIn) {
  LaunchedEffect(Unit) {
     someNavigationLambdaThatWasPassedIn()   
  }
} else {
  Column(){ // my actual layout}
}
or
Copy code
Column(){ // my actual layout, with a buttonClickable
  buttonClick = {
    vm.loginUser(success = someNavigationLambdaThatWasPassedIn)
  }
}
a
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.
c
Can you rephrase that @Alex Vanyo. Not sure I follow?
👍 1
a
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)
☝️ 1
👍 1
1
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
☝️ 1
1
👍 1
j
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 🙂
3
n
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?
👍 1
s
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 ) { } }
a
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.
K 6
💯 7
@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?
s
@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.
c
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!
s
"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).
1
n
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.
3
c
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? 😅
a
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.
c
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.
😂 4
c
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
👍 1