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)
Related
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.
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
I've been using flutter blocs for a while and I have a problem which I don't know what would be the best approach to solve it.
I have a Widget that uses a bloc. This widget has an input text field and a button which fires a network request calling bloc.sendRequest(text). The bloc emits a ResponseState(bool success, string message) depending on the server response. If there is an error the bloc builder will show a pop up displaying the error message and asking the user to change the input field.
The problem comes when the user press the text input after the error pop up is shown. Flutter refresh the builder bloc and the used state is the previous one, which it cointains the error message that has been already shown, causing the builder to show again the pop up. What should be the best approach to tackle this situation? I've thought about some solutions:
Add a timestamp to the ResponseState and do not rebuild the builder if the state is the same as before.
Make the bloc.sendRequest(text) call return the result and show the pop up if required once the Future is completed
Track which pop ups have been showed to avoid showing them twice using a timestamp in the ResponseState
What should be the best approach to solve this? I'm missing something?
Thanks,
BlocBuilder can rebuild at any time so for events that need to be fired only once per state it is better to use BlocListener instead.
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.
Recently i've started covering my project with integration tests where mockito provides presenter instances to verify whether or not my views calling presenter methods properly during their events.
The issue was on the screen which has invisible ProgressBar and RecyclerView. Presenter of that screen have been loading data for that RecyclerView and controlling visibility of ProgressBar. When i replaced it with mock (used Mockito) it caused corresponding tests to totally stuck with error after a while:
Could not launch intent Intent { act=android.intent.action.MAIN flg=0x14000000
cmp=com.example.kinopoisk/com.example.mvp.view.activity.MainActivity } within 45 seconds.
Perhaps the main thread has not gone idle within a reasonable amount of time?
There could be an animation or something constantly repainting the screen.
Or the activity is doing network calls on creation? See the threaddump logs.
For your reference the last time the event queue was idle before your activity launch request
was 1476191336691 and now the last time the queue went idle was: 1476191336691.
If these numbers are the same your activity might be hogging the event queue
But activity was successfully running and was accessible for all user events like clicks etc.
How do you think, what cause a problem?
This is a question for community knowledge base only, i've found answer myself.
The explanation is in that line of error code: There could be an animation or something constantly repainting the screen.
When i had replaced presenter with mockito's one, it stopped controlling progress bar as well. The initial state of it was View.VISIBLE, so that was cause test to hadn't been able to connect.
The solution was just set initial state of ProgressBar to View.GONE, but found it was a bit of headache for me.