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.
Related
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)
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.
When a user signs into my application, an alert dialog appears if it is their first time.
Otherwise, it does not appear.
This makes it tricky when I am trying to write UI tests.
Since the alert dialog appears conditionally, I cannot close it using:
onView(withId(android.R.id.button1)).perform(click())
as I have seen suggested on other posts.
However, if it does appear and I do not close it within my test, the test is blocked from moving on (as it does not recognise any other view ids) and fails.
Does anyone have any recommendations about how I might handle this?
Thank you!
However, if it does appear and I do not close it within my test, the
test is blocked from moving on (as it does not recognise any other
view ids) and fails.
Does anyone have any recommendations about how I might handle this?
Whenever you write a test, you must ensure that the required conditions are in place to reliably and consistently run the test in question. This is the "arrange" portion of the Arrange, Act, Assert idiom.
Therefore, if you're testing a flow that involves the dialog, you must arrange the test to set up the condition under which that dialog shows.
If you're testing a flow that does not involve the dialog, you must arrange the test to set up the condition under which the dialog does not show.
You have not posted code so I have no idea what the "condition" for showing the dialog is, but basically you need to do something in your test that ensures that condition is false if you don't want the dialog or true if you do. Maybe this is setting a shared preference?
So for example, your test might look generally like this:
#Test
fun myAwesomeTest() {
// Arrange - do something to ensure dialog does not show
SomeHelperClass.setConditionToShowDialog(false)
// Act - do actions knowing the dialog will not show
onView(withId(R.id.awesomeId)).perform(click())
// Assert
onView(withId(R.id.duperId)).check(matches(isVisible()))
}
Again, without specifics of your code, it's hard to give more detail but hopefully that is enough to get you going.
Hope that helps!
It sounds like (but I have to guess since you didn't provide more details on this) you're controlling the showing of that "first time" dialog based on a value that you store locally, maybe with PersistentState.
If that is the case, you can control that value directly from your Espresso tests, and thereby make the state of your tests be as expected.
Also, just a side note -- I would strongly advise against having dependent Espresso tests, which it sounds like you have, based on "if it does appear and I do not close it within my test, the test is blocked from moving on".
Your tests must be able to be executed in random order and pass.
I have an app where multiple emergency messages can be received. When each message is received it needs to open a dialog with the emergency details. If more than one emergency is received it will stack the dialogs so that when the most recent emergency message is dealt with and that particular dialog closes, the previous most recent and un-handled emergency should appear, as if they are stacked.
Also, the details for each emergency are updated every x seconds from a service and these changes need to make their way to the correct dialog instance so that, when on screen the dialog is up to date.
I had this working in, I think, an un-efficient way. I was storing the instance of each dialog fragment object in a list and updating that instance with new details and then opening it. This meant that if 50 emergencies were spammed I was storing 50 dialogs. Not great for memory.
Also, this method didn't work well with orientation, where the object is destroyed and re-built, it was taken out of my list and the details reverted to the original details which were stored in it's intent.
I am looking for a way to do this that is as efficient as possible and I wanted to ask your collective Android brain for suggestions.
How would I efficiently manage multiple instances of the same dialog class to get the behaviour I require?
What i would do, is to just hold EmergencyMessage data object stack in my Activity. When stack is not empty, i would show new DialogFragment, and pass it EmergencyMessage from top of the stack, using Fragment.setArguments.
In my DialogFragment, i would use Fragment.getArguments to get EmergencyMessage object and display message. When user deals with message, notify your activity (for example like this) and close dialog. Then, if you still have unhandled EmergencyMessages in your stack, just repeat the process.
This way you always have single DialogFragment instance active at any given time.
Now, regarding updating the EmergencyMessage contents if DialogFragment is already showing this message, you can do it from activity like this:
When you create dialog, pass it tag "EmergencyMessageDialog" so you can retreive dialog later and interact with it.
MyDialogFragment dialog = (MyDialogFragment)
getFragmentManager().findFragmentByTag("EmergencyMessageDialog");
dialog.updateEmergencyMessage(newEmergencyMessage);
If you receive update for EmergencyMessage that hasnt been shown yet, you can just update your EmergencyMessage object in your stack in activity, so that would be easy.
I'm new to Robotium, I have two questions.
1) I'm trying to make click on custom listview item but its not working. I tried with clickInList(int) and clickInlist(int, int).
2) Handling random AlertDialog:
How to handle display alert dialog dynamically in Robotium? For example I'm using alert dialog when I get any message during call webservice, like connection failure, no internet, server error, timeout, etc..,
Thanks in advance.
There are two important things to note about the clickInList(int) method that aren't readily apparent: First, the list items are 1-indexed, so to click the first item of the list, use clickInList(1) not clickInList(0). Second, the clicking is relative to the visible items on the screen, so clickInList(1) will click the first visible item on the list, not the first item overall.
As for the dynamic handling of a Dialog, arbitrary pop-ups aren't really what Robotium was meant to handle. It's supposed to test user interaction with the app under known, controlled, repeatable conditions. If something unexpected happens in the middle of the test, such as losing connection, it should be considered a failure; There's a good chance your test wouldn't be able to run to completion anyway. As a hacky work-around, you can check for the existence of the Dialog before each of your events, something like:
if(solo.searchText("Dialog text") {
//handle closing dialog
}
However, I'd advise against this, it'll slow down your test considerably, and again, even if you close the dialog, the fact that the error happened in the first place is probably going to cause a later part of your test to fail.