hey guys and welcome back to a new video in this video we are going to have a really really deep dive into onetime event on Android so it's a very common thing that we have a view model and we have a UI and we then need to send events from the view model to the UI and this event should only trigger something on the UI once so very typical example would be that you have a login screen and then a home screen or profile screen wherever the user gets to when they are logged in and if you then click on the log button then the view model would of course make the API call or at least trigger something from the repository and then it somehow needs to tell the UI about the result of the login if it was successful it needs to tell the UI it was successful so the UI can navigate to the next screen and this navigation thing should only happen once so we don't really want to model that as state right because state would be something that persists something that the UI would also receive after screen rotation for example but then a lot of people sent me this article from man Vivo who previously was at Google which suggests that these oneoff events are an anti-pattern in his opinion so in this video I will not only talk about why em Manuel weer is is of that opinion but also what I suggest you to do because there are different ways of how we could model such onetime events on the one hand channels and Shi flows and state which is what manual suggests here and I've already been through them all I've used them all for for a period of time I've also stick to the approach manual suggest here for some time but I actually got back from that to the previous approach of using channels and to show you how that could look like in code so we can actually compare these and see why things work the way they do I prepared a little Channel and we expose that channel typically as a flow um with a receive as flow then I prepared a shared flow and I prepared an is logged in state which just compos State and when the user is logged in we can set this to true and the UI will of course also be notified about that and I will now go through all these three ways of letting the UI know something when it comes to navigation here and highlight their advantages and disadvantages so when the user clicks the login button we trigger this function we launch a coroutine in view model scope we set these loading value to true and then we simulate some kind of login API request with a delay function and when that is done and the login was successful we want to send something in this Channel and that something is a navigate to Profile Event so that is just how we commonly model this if we want to send such navigation events from A View model to the UI we just have such a sealed interface for example with all the different Navigation actions we might want to send to the UI and this is just something the UI can then observe it can collect these events and then trigger the navigation the difference to a real State flow or to a real compos state with a channel is that these events are just sent once so they won't really be cashed anywhere if they have been collected and therefore they also won't be sent to the UI again when we rotate the device or when we change the device theme so we go to our main activity where we just have a very simple nav host with our login and profile route and we're currently here on the login screen which you can also see here we only consists of a button and when we click that and then we see a loading indicator for 3 seconds and then after that this appeared we actually want to perform the navigation but right now we don't collect any of these events from the channel so let's change that in compos that works with a launch effect so launch effect which is just an effect Handler with which we can safely collect this flow from the view model here we need to get a reference to the life cycle owner which is local life cycle oned current because whenever the life cycle changes we want to also recall this launch defect block and then in here we can say life cycle owner that repeat on life cycle which requires a special dependency uh but that is just the safest way to collect flows um by making sure that these flows also won't run when the app is in the background so here we want to say life cycle. state.st started and then in here this will be re-executed when the um app gets back into the St state so here we can now safely collect the flow which comes from view model dot navigation events Channel flow and we want to call collect and here we get all these events we can then check what kind of event that is and if it is a navigation event. navigate to profile then we want to perform the navigation here with our na controller that navigate and we navigate to the profile road if we launch this take a look here then clicking login we should hopefully see that the navigation Works yes after 3 seconds we get to the profile screen we can perfectly navigate back which usually wouldn't be what you want in in this scenario with a login function but uh that just to show you that the back stack works perfectly fine we could also extract this piece of code here into a little extra function so we don't have that much code every time let's cut this out have a composable function here for example observe as events we then pass in a flow which we want to observe as events here we can introduce a generic variable so we can use this with any type of flow and we want an on event Lambda which gives us the events we've collected then we can paste our stuff here we can also add the flow here to our launch def block so in case that changes for whatever reason and here we then say flow. collect can cut this out and just say we pass the onevent Lambda to our flow collect function up here we can then say observe a state observe as events of course the flow is again view model. navigation events Channel flow and then we just get our events here as usual and we can put in our when block and that will now work exactly the same as before something worth noting is if we relaunch this we log in and then we minimize the app if we now go back we land at the profile screen that is something really worth noting because channels effectively have an integrated buffer because as you noticed even though our app was in the background where this block here shouldn't actually be active because we have this line of code here even though that was a case the flow collector still received the event and that is because channels have an integrated buffer so if there is no collector and we send an event into a channel then it actually ends up in a buffer and as soon as a collector appears again that collector will then also receive that event that was previously uh saved in that Channel's buffer when we now take a look at the next typical way people handle this Shar flows we can simply comment this out and also maybe comment it with Channel comment this out and then in here we can have our navigation events share flow which is actually this one so the mutable one here we need to call emit instead of send but other than that it really works exactly the same as with channels then the exposed one is this navigation event share flow so we need to make sure in our UI that we also observe that one so navigation events share flow if we now relaunch this take a look here click login wait 3 seconds then we should actually see the exact same result we also get to the profile screen we can perfectly navigate back if we do this and minimize the app and then after 3 seconds come back we stay at login so that behavior is different compared to channels because shared flows in contrast to channels do not have such a buffer by default and that means if there is no active collector of the Shar flow and we still send something into it then the event is just lost we can still kind of set such a buffer by going to our view model the share flow Constructor and here we can set such a replay cache for example to three and if we then relaunch this take a look here login minimize the app now go back then we landar at profile because with this replay cach we can say that three events should be cashed if there is no collector so with this we really get similar Behavior as with the channel but Shar flows as the name suggest is rather meant if you have multiple subscribers to a single flow while channels on the other hand yeah are just meant for a single subscriber which you typically also have when you when you have a UI that subscribes to a channel then there's typically not a scenario where one screen would subscribe to the same channel twice but let's now get to the third way of doing this and that is with state that is also what this article suggests because that really says one of events are an anti- pattern which really refers to channels and share flows and what I think about that and how I handle these kind of things I will get to later in this video but let's first of all understand why Manuel things that one of events are an anti-pattern because it's a valid concern when you read this article the main problem that is talked about here is that these onetime events aren't guaranteed to be received by the UI so here as you can see these apis like so Channel and share flow don't guarantee the delivery and processing of those events that is the pro the problem that is really highlighted here now I'll also show you a scenario in this video where this really is the case where an event sent into a Channel or Shar flow is lost but long story short this article suggests that we model these onetime events as state and that is what I did here so I prar then is logged in state which is just a normal compost State and we could of course we comment this here Shar blue we could of course just have our state update here where we say is logged in is now true and then in our UI let's comment this out for now now we would just have a launch defect block where we listen to um actually just the state that is logged in and then we can check if that's the case so if we are logged in then we perform the navigation have controller navigate to profile if relaunch this you will see something that feels familiar we click login wait 3 seconds and then we do get to the profile screen but what now happens if we navigate back then oh we kind of stay at the profile screen and that is of course the disadvantage of when using State because the backstack entry and the view model from the previous screen is not described when we navigate to a new screen and that means all the state is kept including this is logged in state so it's still set to true and as soon as we navigate back to the screen this launch effect block will trigger because is logged in is still true and it will navigate back to the profile screen so what we would need here in order to make this fully work is an additional function in the view model um for example here oops here a function to on navigated navigated to login for example or we just say state is equal to State copy is logged in is set back to false if we call this function directly after navigating here your model on navigat to log in launch this take a look here click login wait a few seconds and then go back then it suddenly works so now we went through the three ways how we could handle such one-time events the first two so channels and share flows both have the issue that events could potentially get lost and in a moment I will get to why and in which case and modeling it as state doesn't really have the problem that state could go lost because State just is State just it's just persistent but that on the other hand often has a problem that you need to actively think of resetting that state in a scenario you might want to log in and navigate or let's think of a scenario where you want to show a toast or a snack bar then you would also need to reset that right afterwards which is not super obvious in my opinion but let's not get to when such events sent into a channel would be lost and same counts for share flows of course and for that we need to understand how these events are collected here in our UI let's first of all comment this out and comment this if we take a look at our observe as events function then here we have this repeat on life cycle function which makes sure that the flow the anling flow does not trigger when our app is in the background that is why we pass the lab cycle State started so that we resubscribe subcribe to that flow when the app is back in the the started State and we unsubscribe from the flow in I think it is on stop but what that also means is that there is a window where there is no active collector and that is for example when the app is currently in the destroyed state if the view model send such an event when the activity is in the destroyed State then it's lost and I want to show you that with a little demo so we could go in here in our view model and let's comment out this entire loged in function and replace it with something like this we have a view model scope in here we say repeat 1,000 times and we just delay this for 3 milliseconds and we could also extend this navigation event with something like a count event so I now want to have a counter which spam this Channel with events so count event well count oops like this and for a thousand times we say we have our navigation Event Channel um navigation Channel this one here that's send with it so with the current uh with the current number here from our repeat block need to WRA it into a account event and then we can receive these in the UI and print them so if there is really an event that is lost we would see it in our loat because there would be a gap there would be numbers that weren't printed so here in main activity we want to go to our observe as events block also check for count events if we get such an event we want to say print line count and we print the current count that was sent with this event and let's also have on destroy that we override here I would like to print count interrupt because obviously when we rotate our device for example or when the activity gets destroyed in whatever context then on Destroyer will be called and the Subscribe or the the um UI will unsubscribe from this flow and will then resubscribe as soon as the activity is back in the started State and if we launch this now take a look here then we should also go to locket first of all searching for count clearing this clicking log in rotating this rotating this and for some reason it does not print these counts am I doing something wrong ah yes because I'm still subscribed to the share flow let's switch this to our Channel flow relaunch this take a look here uh clear lck cat if we click log in then it's spaned we wrote it a few times to move the activity in the destroyed State and let's now scroll up where is the first count interrupt um it's here okay here it seems like no event was lost because before on Destroy we got um 271 and then we got 272 let's move on to the next one where is it did I miss it here yes count interrupt okay here it also seems fine here it also seems fine um maybe just an unlucky scenario here um let's try this again log in rotate rotate rotate like this go to log cat and scroll up ah there it is so you can see here we got count 844 then we got the count interrupt and then it resumed with count 846 so the event for 845 was effectively lost here and this is really the problem this article from Manuel talks about that it is simply not guaranteed that the UI will receive such events so what do I now suggest you to do um that is really just my personal opinion other people might agree and might disagree but nowadays I've tried out all these different possibilities in real apps I've used Shi flows for SE onetime events I've used channels and I've also sticked to using persistent state for a while first of all what what I would recommend the least are share flows because they are effectively Channels with a disadvantage that you need to manually add a buffer and then is also the question what kind of value do you use for the buffer since you can simply have an unlimited buffer for UI that buffer is really helpful so I would just use channels they are meant to work with only a single subscriber which is the case when you send events to a UI so share flows would be out here for me so now the battle is only going on between channels and state what I really like about channels is that they are really just sending something to the UI once that is a concept every developer can easily understand and that is also something that makes sense to me when it is about such navigation events when it is about showing a snack bar or similar things what I absolutely dislike about having state for such onetime events is that you always need to think about resetting that state just like we did here not always um in this exact scenario you actually would need to do this because typically when the user logs in then you clear the whole back stack before because you you don't want them to be able to go back to the login screen but very often you also have these navigations where you want to keep the previous screen on the back stack and always in these cases you need to reset the state when I think of a larger team maybe where there might be junior developers as well then I believe it's pretty hard for allel Vel opers to always stick to this Rule and to always remember resetting that state after observing it I think that always needing to reset the state is much more likely to lead to a buck than this super rare scenario that an event gets lost when being sent into a channel because as you just saw we sent a thousand events into a channel and we even had to do it twice in order to see that an event was lost and do you know what the best thing is there's even a solution so we can prevent events from being lost and I want to show you that solution very quickly by going into observe as events if we go in here and we change the cortin context to dispatchers main that immediate oops and collect the flow inside of that immediate dispatcher and we also make sure that events that we send into this channel are always being sent on the main dispatcher which would be the case here by default then losing events cannot happen and if we want to understand why we need to understand the differences between dispatchers Main and dispatchers main. immediate so typically when it comes to Android Android's UI is event driven so that means you just have a series of events these events could be drawing something on the UI processing a touch event for example that could be an activity life cycle event just like on Destroy here and if we use this patches main so without immediate it will simply queue in the action from the Curtin in that event queue from Android itself but if we switch to the immediate dispatcher that dispatcher will make sure that if an event is sent here into this Channel and received on this flow that it is processed in the same event cycle Android is currently on and by doing that losing such events cannot happen because this immediate dispatcher makes sure that the item from the channel is processed before view destraction can happen I know that that is probably very complex and you also don't need to understand that in detail but know that if you collect such events in the UI you need this with context block to be sure that you can lose events and yes that might be considered a workaround that might be considered a hack because it's really probably not meant to be used like that and in my opinion it's also not ideal that we have to use this in order to make it work in all scenarios but I thought I think it's much less ideal to always needing to reset State like here so with this approach you just have one function which looks little bit complex and much more complex than um you might think it is to Simply collect the flow but that is what you what you need if you want to do it right but from this point onwards you can just use this observe as events function across your project and you can be sure that no events will be lost I also want to show you this um with this little change here we still send these uh thousand events in there we can relaunch this here take a look login rotate rotate rotate something like this take a look here where that was interrupted you can see here no event was lost if we scroll up to the next interrupt then here no event was lost either if we take a look here to the next interrupt where is it um here 513 and 14 no event was lost and to the probably last one 271 and 272 no event was lost either so that really now works and events cannot be lost anymore I know this was a very long and detailed video and I hope it you still enjoyed it and learned something I think such a buck of an event being lost maybe happens in one in 10,000 cases but it's of course still worth it to to deal with it and to to learn about it so that this can't happen and you can build more stable apps and if you think there might be more such issues in your current code base but you just don't know where then it might be very worth to apply for my 10we Android mentorship program where both of us will be working together really closely to take a look at your code base and your struggles so we can eliminate these over the course of 10 weeks you will find the link Down Below in this video description applying is completely free if it's a fit we get to talk about it um if I can really help you and other than that thanks so much for watching this long video I will see you back in the next one have an amazing rest of your day [Music] bye-bye