flutter and bloc asynchronous yield - asynchronous

I am using flutter with the BLoC pattern (with the flutter_bloc library) and I have these events:
PersonalFileAddedEvent(File file), PersonalFileUploadEvent(PersonalFile file) (both extend from PersonalFileEvent)
File is a file from the file picker, PersonalFile is a class that has these enum statuses: READY_TO_UPLOAD, UPLOADING, UPLOAD_FINISHED.
And this status from the BLoC:
PersonalFileListLoadedState(List<PersonalFile> files) (extend from PersonalFileListState)
When the user selects a file, the UI calls the event PersonalFileAddedEvent and passes it to the BLoC which creates a PersonalFile object and sets it's status to READY_TO_UPLOAD. This PersonalFile object gets
added to a list which holds all the PersonalFile's that the user is adding (and uploading). The BLoC then responds (yield) with the PersonalFileListLoadedState(blocPersonalFileList) to the UI to render the information.
Once added, an "upload now" button gets rendered in the UI for that PersonalFile. When pressed, this calls the PersonalFileUploadEvent event and sends along the PersonalFile to the BLoC to start the upload process (multipart upload).
Immediately after receiving the event, the BLoC updates this PersonalFile's status to UPLOADING and yield the PersonalFileListLoadedState state with the PersonalFile's status updated for the UI to show that it is uploading.
The method that uploads the multipart file is async:
Future<PersonalFile> upload(PersonalFile file) async { //upload code }
This is the mapEventToState from flutter_bloc:
Stream<PersonalFileListState> mapEventToState(PersonalFileEvent event) async* {}
Inside this mapEventToState method I am awaiting the upload method to update the PersonalFile's status to UPLOAD_FINISHED.
The problem starts now, as the user adds several files from the file picker, and presses all the "upload" buttons. The BLoC gets blocked after receiving the first event and processes the events "synchronous like" and the UI
remains as if the other's PersonalFile's "upload" button were not pressed until the first one finishes (then the next one, and so on).
After one event gets fully processed (the upload completes), the next one gets processed, which makes sense since I am awaiting for the upload method to finish.
How can I write this code so that if the user presses several "upload" buttons in the UI, the BLoC does not get blocked (and thus the UI because the BLoC is unable to yield the new state) and all the files get uploaded
in parallel but only until each one finishes the BLoC sends the new List of PersonalFile with their status changed to UPLOAD_FINISHED?
I have tried changing the upload method's signature to:
Stream<PersonalFile> upload(PersonalFile file) async* { //upload code }
and using:
.then((file) { yield PersonalFileListLoadedState(listWithUpdatedPersonalFileStatus) })
but the code inside never gets executed. I tried debug but I cannot reach the breakpoint.

Not sure how are you calling the bloc to start the process upload.
But if you are using bloc.add(event) when the button is pressed it should be processed in an asyncronous way and yield the new statuses as needed.
Also notice that when you are processing the status change if the same state is yielded several times, the listener will listen to it only once so you wont see more than one update in your UI.
Hope this helps.

Related

How can I prevent twice call of observe() method in onViewCreated()?

I have Fragment where in onViewCreated() requesting data ,
So every time if I return to that fragment it checks if the mutableLiveData is empty or not and if it is empty it requests from server and I am observing that data in onViewCreated()
But if I navigate to another tab and then return to the same fragment observe() calls twice
I don’t want to move requesting method in onCreate() as if the request fails and if I navigate to another tab and return to that again I want to rerequest the data from server.
My question is how can I prevent call of observe() twice ?
I added removeObservers() in onDestroyView() but it doesn’t help
Android recently introduced the viewLifecycleOwner in fragments. By using it your observe method, this will prevent it from being called twice. Do not use activity or fragment object, use the fragments viewLifecycleOwner object instead.
No need to removeObservers().
example code:
viewmodel.livedata.observe(viewLifecycleOwner, Observer {
// your code here
})

How do I dismiss a WKInterfaceController that was presented with contextForSegue?

I have a WKInterfaceController with a WKInterfaceTable that lists some events a user has recorded in my app.
A user can tap a row of the table to see more details about that event. To accomplish this, I've overridden contextForSegue(withIdentifier:in:rowIndex:) on the WKInterfaceController that contains the table, so tapping a row modally presents a detail view of that row in a new WKInterfaceController called EventDetailController.
The modal presentation is defined on the Storyboard. I can't use push presentation because the WKInterfaceController with the WKInterfaceTable is a page among multiple instances of WKInterfaceController at the top level of my app.
Here's the main issue:
Within the EventDetailController, there's a Delete button to destroy the record that the table row represents.
When a user taps the Delete button, I present an alert that allows the user to confirm or cancel the delete action.
Once the user confirms the record deletion, I want to dismiss the EventDetailController since it's no longer relevant, because it represents a deleted record.
Here's the IBAction defined on EventDetailController that gets called when the Delete button is tapped:
#IBAction func deleteButtonTapped(_ sender: WKInterfaceButton) {
let deleteAction = WKAlertAction(title: "Delete", style: .destructive) {
// delete the record
// as long as the delete was successful, dismiss the detail view
self.dismiss()
}
let cancelAction = WKAlertAction(title: "Cancel", style: .cancel) {
// do nothing
}
presentAlert(withTitle: "Delete Event",
message: "Are you sure you want to delete this event?",
preferredStyle: .alert,
actions: [deleteAction, cancelAction])
}
The problem is that watchOS doesn't seem to allow this. When testing this code, the EventDetailController does not dismiss. Instead, an error message is logged in the console:
[WKInterfaceController dismissController]:434: calling dismissController from a WKAlertAction's handler is not valid. Called on <Watch_Extension.EventDetailController: 0x7d1cdb90>. Ignoring
I've tried some weird workarounds to try to trick the EventDetailController into dismissing, like firing a notification when the event is deleted and dismissing the EventDetailController from a function that's called from an observer of the notification, but that doesn't work either.
At this point I'm thinking there's some correct way I'm supposed to be able to dismiss a WKInterfaceController, or in other words reverse the contextForSegue(withIdentifier:in:rowIndex:) call, but I don't know what it is.
When I call dismiss() directly in the IBAction, instead of in a WKAlertAction handler, it works fine, but I don't like this implementation since it doesn't allow the user to confirm the action first.
I feel like an idiot, but I figured out the solution.
The answer was in Apple's WKInterfaceController.dismiss() documentation the whole time (emphasis added):
Call this method when you want to dismiss an interface controller that you presented modally. Always call this method from your WatchKit extension’s main thread.
All I had to do differently was call self.dismiss() on the main thread.
Here's my updated code for the delete action, which now works as expected:
let deleteAction = WKAlertAction(title: "Delete", style: .destructive) {
// delete the record
// as long as the delete was successful, dismiss the detail view
DispatchQueue.main.async {
self.dismiss()
}
}
Hopefully this will save someone else some troubleshooting time!

How can I start another request after AbortController.abort()?

I've read about cancelling fetch requests by using AbortController.abort(). Is there a way to start a request again without aborting it after calling this command?
For example, in this demo from MDN, once Cancel download has been clicked, clicking Download video will trigger the fetch again, but immediately abort it.
Is there a way to allow this request again without aborting it? So, in this case, how could you click Download video to begin the download, click Cancel download to cancel the download, and then click Download video again to start the download again? For example, if the user clicked Cancel download on accident...
You can't.
An AbortController or its signal can not be reused nor reseted. If you need to "reset" it, you have to create a new AbortController instance and use that instead.
I think this is by design. Otherwise it could get messy e.g. if you hand over the controller or signal to some external library and suddenly they could remotely un-abort your internal state.
For example, in this demo from MDN, once Cancel download has been clicked, clicking Download video will trigger the fetch again, but immediately abort it.
They fixed the example. After you click Cancel download you will be able to start a new download and cancel it again, over and over. In order to achieve that the Download button instantiate a new AbortController every time, so you get a fresh signal to abort every time:
downloadBtn.addEventListener('click', fetchVideo);
function fetchVideo() {
controller = new AbortController();
signal = controller.signal;
// ...
So it's ok to instantiate new AbortControllers for each request that you may wish to cancel.
I know this might be kind of late, but I'm leaving this answer anyways in case someone needs it.
I don't know if this is the most optimal approach, but in order to keep doing fetch requests (with 'the same' signal) I had to create a new AbortController instance for each request.
In my case (all code being contained inside a class declaration), I deleted and created a new instance every time, like so:
class Foo Extends Bar {
abort_controller_instance = false;
constructor(params){
super(params);
this.resetOrStartAbortController();
}
resetOrStartAbortController(){
if(this.abort_controller_instance){
delete this.abort_controller_instance;
}
this.abort_controller_instance = new AbortController();
}
abortFetchRequest(){
if(this.abort_controller_instance){
this.abort_controller_instance.abort();
this.resetOrStartAbortController();
}
}
...
}
Probably it's not the most elegant solution, but it works.
Regards!

Do we have any lib to capture the state changes using sagas?

Basically need to build a warning modal , when user tries to move from current page/screen to another page , showing there are some saved changes .
Any implementations using redux and redux saga
Sagas are the lib for this - they watch for any action of a specified type. Navigation will take two actions: one to indicate that navigation is about to happen (which the saga will watch) and one to actually update the current page. The saga watches for actions of the first type and shows a warning dialog if the data has changed.
Ex:
function showWarning(action) {
if (/* data has been changed but not saved */) {
displayWarningDialog(action.pageToNavigateTo)
}
else {
// action that updates the page/location
completeNavigation(action.pageToNavigateTo)
}
}
function* mySaga() {
// NAVIGATE_TO_PAGE_X are the actions that get fired when a user changes pages
yield takeEvery("NAVIGATE_TO_PAGE_1", showWarning)
yield takeEvery("NAVIGATE_TO_PAGE_2", showWarning)
}
There is the amazing Redux DevTools for state debugging. This tool was built by Redux author himself.
Here are its features
Lets you inspect every state and action payload
Lets you go back in time by “cancelling” actions
If you change the reducer code, each “staged” action will be
re-evaluated
If the reducers throw, you will see during which action this
happened, and what the error was
With persistState() store enhancer, you can persist debug sessions
across page reloads
I've thought about this recently as well and been thinking about writing some form of middleware to intercept routing actions.
When intercepting a routing action, the middleware could determine if application state indicates the user is editing some unsaved data, and if so, dispatch a different action instead. That action should reduce state and cause a warning to render. The user could then confirm wanting to continue navigating by dispatching an action also intercepted by the middleware to continue the routing process.

In Meteor, how do I show newly inserted data as greyed out until it's been confirmed by the server?

Say my application has a list of items of some kind, and users can insert new items in the list.
What Meteor normally does is: when a user inserts an item in the list, it appears in their browser immediately, without waiting for server confirmation.
What I want is: when an item is in this state (submitted but not yet acknowledged by the server), it appears at its correct position in the list, but greyed out.
Is there a way to make Meteor do this?
Sure. Make a method that does the insertion. When the method runs, have it check to see if it is running in simulation, and if so, set a 'temporary' or 'unconfirmed' flag on the inserted item. Use that to decide whether to render the item as greyed out.
Assuming you're using MongoDB:
// Put this in a file that will be loaded on both the client and server
Meteor.methods({
add_item: function (name) {
Items.insert({name: name,
confirmed: !this.isSimulation});
}
});
Calling the method:
Meteor.call("add_item", "my item name");
That's all you need to do. The reason this works is that once the server has finished saving the item, the local (simulated) changes on the client will be backed out and replaced with whatever actually happened on the server (which won't include the 'unconfirmed' flag.)
The above is the simplest way to do it, but it will result in all of the
records in your database having a 'confirmed' attrbiute of true. To avoid this, only set the confirmed attribute if it's false.
Refer to this part of documentation for more information about isSimulation and Meteor.methods
This is what I did added an observer on the server side,
I created a variable called notify false from the client side itself
once the server receives the udpate it will make notify true and the client will be updated on the same.
Collection.find({"notify":false}).observe({
"added" : function(first){
collection.update({"_id":first._id},{$set : {"notify":true}});
}
});

Resources