Observe runs multiple times on button click for the second time - android

I have this code in an activity SignInActivity:
signInButton.setOnClickListener{
val query: HashMap<String, String> = HashMap()
query["email"] = signInEmail.text.toString()
query["password"] = signInPassword.text.toString()
signInViewModel.getAuthToken(query)
signInViewModel.signInResponse.observe(this, {
response-> when(response){
is NetworkResult.Success ->{
response.data?.let { Toast.makeText(this, it.access, Toast.LENGTH_SHORT).show()}
}
is NetworkResult.Error ->{
Toast.makeText(this, response.message.toString(), Toast.LENGTH_SHORT).show()
}
is NetworkResult.Loading -> {
}
}
})
}
Let's suppose in the first try I wrote my password wrong and it only runs once, but then after that if I click it again it runs multiple time by creating multiple toasts in this example.

Like #gpunto says, you're adding a new Observer every click, so they're stacking up and each one fires when the LiveData updates.
But really, the observer doesn't have anything to do with the actual click anyway, it just receives updates to signInResponse and displays a thing. The click just calls getAuthToken with the current query. If doing that happens to cause a signInResponse update, then you have everything wired up to react to that event. But the Activity doesn't need to know how all that stuff works, or be written so one thing follows another.
That's a reactive pattern, where your UI is really just sending events (like getAuthToken when there's a click) and then reacting to other events so it can display them. By separating these things, you get a simple system that Just Works, and can react to updates no matter what caused them (e.g. a click, or restoring state) without having to write code to handle each case.
That said, this is a slightly tricky case because you have an event you want to consume. If you just set up that observer on signInResponse, it will fire every time you get a value for that LiveData. And that includes when the Activity is recreated (e.g. on rotation), observes the LiveData, and gets the current (last-set) value. Basically, if you show a Toast, the same Toast will appear every time the Activity is recreated. That would be fine for setting the current value on a TextView, but it's bad for a popup that should only appear once.
This is the current official recommendation for handling this situation. They're creating a UI state, which basically holds everything that needs to be displayed, including any popup messages (which acts like a queue, which is useful!). When the UI displays a message, it basically tells the ViewModel it's done so, and that handles removing the message from the state.
You could just implement this your own way, even if it's something simple like a clearResponse() function in your VM that clears the current value when you've seen it. It really depends on your app and what state you need to maintain. Here's some other examples from the Android devs - but like it says at the top, this advice is deprecated following the recommendations I linked earlier

Related

Snackbar message being replicated

I have created a bottom navigation bar in which i have three fragments, for now,let's say fragment 1, 2 and 3. I have enabled a live data observer and it shows a message whenever an api returns the error message. The message is then shown to the user via the snackbar. I had some issues while showing the messages as my app was crashing. I have rectified the error.
The app is no more crashing but I ran into another problem. Let's say there is an error message "User not found" in fragment 3. The message is displayed in the snackbar. But when I navigate back to the fragment 1 or 2, the same error message is displayed in the snackbar. I have checked the api response and there is no error response.
private val errorObserver = Observer<Int>{
activity?.let { it1 -> Snackbar.make(it1.findViewById(android.R.id.content), it, Snackbar.LENGTH_SHORT).show() }}
This is the code I used to solve the initial problem of crashing. I don't know how to solve the second one.
Your problem is probably (you haven't posted much code) that you're holding your error state in something like a LiveData or StateFlow. When your Fragments start and begin observing that error state, if there's a current value then the observer receives that immediately, and handles it (e.g. by showing a Snackbar).
This is fine when your UI is meant to be updating to display the current state, but it seems like your errors are an event, something transient that occurs and then goes away. You basically need to clear that error value once it's handled, so that anything that observes that observable won't see that same error.
People in the comments are mentioning the SingleLiveEvent approach, but that's an old workaround that the Android team considers an antipattern now. The recommended way of consuming UI events is explained here, but it basically goes like this:
In a ViewModel, some UI state object (e.g. an error state, or an object describing the entire UI with an error state field in it) updates to hold an error value
an observer sees this change, handles the error (e.g. displaying a message), and then tells the ViewModel the error event has been consumed
the ViewModel updates the error state / UI state again with the "no error" state, or whatever (maybe the next error if there's a queue of them)
So in your case, as soon as you display the snackbar, you'd tell the ViewModel (or however you're doing things) to clear that error, because it's an event that's been handled. This is different from a persistent error state, e.g. showing a warning icon if there's a problem that needs addressing, which you'd want to show all the time (until that error state changes)

Android kotlin flows collects initial values when visible

I was trying out different kinds of flows like flows with channel, sharedflows and stateflows. What I did was, suppose I have a MainActivity, inside it I have two buttons side by side at the top and below them a fragmentContainerView. Initially the fragmentContainerView doesn't have any fragment.
Now I have a viewModel where I am emitting a range of int values in a loop with 1 or 2 seconds delay with all three flow types. And I have consumers of the values in MainActivity, fragmentA and fragmentB (fragmentB has collectLatest in all three flows when collecting). Clicking button1 attaches fragmentA and Button2 attaches fragmentB.
Now what happens after the values are started emitting suppose initially from 0. The mainActivity starts receiving as soon as the values are emitted. Then when I click button1, fragmentA starts receiving from initial value 0. After sometime I click button2 which removes fragmentA and attaches fragmentB, now fragmentB starts receing from value 0 which has collectLatest. Again if I click button1, fragmentA starts receiving from initial value 0.
I can understand that when the fragments are not visible they should not receive any values. But I want to understand is this the intended behaviour like whenever a new fragment is coming visible its receiving from initial value instead of having collectLatest which did not work. Am I doing anything wrong or why is it happening like this? Are the previous initial values stored in some form of cache? and if I somewhere want to get the current latest value when the view is visible, in what way can I do it? Guidance with some sample code will help. Thank you
Fixed the problem:
Actually I made a mistake by creating new instances of the viewModel in fragments, and it was the viewModel where the values were getting emitted. Fixed it by getting the MainActivity's viewModel instance everywhere.
Sounds like you are using cold flows instead of hot flows.
The behavior of cold flows is that each new collector gets values starting from the very beginning (the flow producer starts a new production process for each collector). For example, if you use the flow Flow builder, like this:
val flow = flow {
for (i in 1..3) {
emit(i)
delay(100)
}
}
Then each time a coroutine calls collect on it, that coroutine will get a fresh new stream of values, starting from the beginning of the above lambda function.
With a hot flow, the behavior depends on the implementation. Channel-based flows fan out, which means no two collectors will ever receive the same value. For each value emitted, only one collector will receive it. SharedFlows can have a buffer that replays up to a certain number of past values for every collector. A StateFlow behaves like a SharedFlow with replay value of 1. Each new collector can only collect the most recent value followed by any further latest values, and if it is slower at collecting than values are being produced, it will skip values.
The generally recommended type of flow to use in a ViewModel that fits most uses is a SharedFlow with a replay buffer of 1, and if based on an upstream flow using shareIn, a SharingStarted of WhileSubscribed(5000). This is a hot flow, but new subscribers get the most recently emitted value from the replay. So if the screen is rotated, the most recent value is still in memory and can be immediately displayed in the UI. The SharingStarted.WhileSubscribed(5000) allows it to stop collecting from the upstream flow when there are no more views on screen collecting from it, but the 5 second buffer waits to make sure it's not just a screen rotation causing a very temporary lack of subscribers.

Android ViewModel: How to get a result from the Activity (best practice)?

I have the following situation:
The user opens a dialog in which the user enters information
The user closes the dialog and returns to the default UI
The data is send to the backend
Now I wonder how to implement this in a good way. At first, I think the Activity calls a method in the ViewModel to trigger an event. Secondly, the ViewModel updates an LiveData-object to trigger an observer in the Activity which opens the dialog. But after that I don't know how to implement the rest in a "best practice"-way. My current implementation is that, when the user closes the dialog, the Activity calls a "finish"-method in the ViewModel and hands over the data from the dialog as arguments. After that the ViewModel updates the LiveData-object and the event is over.
You don't specifically need to have a flow Activity -> ViewModel -> Activity, when you're just about opening a dialog. It would make sense if you have to get some info (for example price) from your back-end side and include it into your dialog description. If it's just a simple UI stuff like "show-me-a-dialog", then it's fine to leave it on your UI layer only.
And when the dialog is closed, you'll just pass needed arguments to VM, make your back-end request, and in this case it's logical to emit some event to your Live Data and it's a common practice.
Android has a bunch of really great articles and they were added no so long ago, take a look here (picture was also taken from the article). They have clear and simple explanation about all UI-VM-Data/Domain communication and how to do that it in the "right way".

Compose not listening to any repeated value

I have a screen listening for a data class that contains everything I need. ScreenState. Whenever the user press a button I send the event to a ViewModel. This specific event is just getting the intent and setting on the ScreenState parameter like this.
screenStateFlow.emit(
ScreenState(
Intent(...)
)
)
What happens there is, first time works (User leaves the app and then comeback to the app). When user comebacks to app and there's not any data from the intent and want them to be able to start an intent again. So it does the same action.
Triggers a specific event which gets the intent and sets on the ScreenState parameter and this value is emited, again
And here lays the problem. Value is the same. So compose doesn't recompose itself.
And this solution works. You could say that I don't need all of this and it could work by just starting the intent without having to go through the event process and etc.. But I want it that way (unless I don't find a proper solution)
screenStateFlow.emit(
ScreenState(
Intent(...),
!triggerRecompose
)
)
Is there any better solution?
Edit: Someone having the same issue as me, the provided answer didn't work. I've already tried the MutableState and the State from compose in ViewModel. Didn't work
I had a similar issue in which I wanted to trigger a snackbar even if the value is repeated.
I solved it by adding a variable parameter to my message object (such as timestamp or Math.random()).
In this way, even if the message content is the same, the Message object is different and it triggers a state change.

Handling long running tasks with RxJava

I'm trying to migrate an AsyncTask that sends messages to the server, to use RxJava. Roughly, the task does the following:
1) Creates a message that will be sent (persists to the database)
2) Shows the message to the user (state 'sending')
3) Sends the message to the server (code snippet below)
4) Marks the message as sent or failed (persists to the database)
5) Updates the UI
I've created the required Rx chain which partially looks like this:
public Observable<Message> sendMessage(Message message) {
return mApiClient.sendMessage(message)
.doOnNext(sentMessage -> mDatabase.synchroniseMessage(sentMessage))
.doOnError(e -> {
message.setState(FAILED);
mDatabase.synchroniseMessage(message));
})
.onErrorReturn(e -> Observable.just(message));
When I subscribe to the above, I get a Disposable. Normally I'd add it to the CompositeDisposable object and clear that object then the user has moved to a different view (i.e. fragment). However, in this case, I need to keep running this task to make sure the local database is updated with the task results accordingly.
What would be the most appropriate way to handle this situation? I could simply not add the Disposable into my CompositeDisposable object and therefore it wouldn't be unsubscribed from, but it feels like it could cause issues.
P.S. Showing updates to the user is handled through observing the data in an SQLite table. These events are triggered by the synchroniseMessage method. This is a different subscription which I will simply unsubscribe from, so it's not part of the problem.
One disposes of Disposable as soon as he is no longer interested in it.
In your case you are still interested in the stream regardless user navigates to another screen or no, which means you cannot unsubscribe from it. Which means you cannot add it to CompositeDisposable.
This will result in a situation, when your Activity cannot be garbage collected, because of a implicit reference to it from your Subscription, hence you are creating a memory leak situation.
If you have such a use case, I think you have to perform that request on a component, which will be activity lifecycle independent, like Service.

Categories

Resources