Return data when a screen is closed - flutter

I have several sub-screens which give the user the option to save some data. When that screen is closed, I want the parent screen, which pushed the sub-screen, to know whether data was saved or not. The sub-screens maintain a didSave flag and are set to true when data is saved.
There are several ways for the sub-screens to be closed:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
I can handle the 3rd case using Navigator.pop(context, didSave) and in the parent that didSave flag is captured using final didSave = await Navigator.push<bool>(context, myRoute).
However, for the first 2 cases, the result will obviously be null.
I have looked into WillPopScope but that only is used to determine whether the screen should be closed or not. It doesn't seem that I can set any data to be returned for the push call.
I have also looked into wrapping the parent screen in a Provider where it can listen to didSave states but that will trigger immediately when emitted which is not desirable for my use case. I only want to act when the sub-screen is closed not when data is saved.
I can potentially use WillPopScope and emit an event there if a save operation has occurred but I would like a simpler solution if available.
Is there a way for this that I am missing?
Thanks!

as you said the Provider will listen to didSave and this doesn't fit in your case you can use just a simple inheritedWidget:
wrapping the parent like this:
InheritedExampleWidget(
didSave: false,
child: Parent(),
),
you need to set a setter to didSave
then on the ascendant widgets on the widget tree, you can:
InheritedExampleWidget.of(context).didSave = true;
this will not trigger it immediately, which the Provider package solves.
then you can manage the state however you want

Did you try to create the variable with state management? And there is method with the push that do task after user come from the child screen. So, when they come you can checkout the use have saved data or not.
For EG:
saved = false; //State management variable
//We are pushing on another screen.
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
new ScreenName(),
),
).then((val) {
//This part with work when user popped the screen on which we have pushed
if (!saved) {//Do Something when not saved}
});
Try above code and let me know if you get any error or you're facing any issue.

when you push a new route, using StatefulWidget, it will have a lifecycle starting from an createState, when the widget isn't there on the widget tree ( when we pop ), the dispose method will be called.
those cases of popping the route:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
will trigger the dispose method to execute.
so you can put inside it your logic that you want.
exmaple :
class classTest {
bool didSave = false;
}
then when on the property where you want to push the screen set it to that classTest's didSave, as an example:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const YourRouteWidget(didSave: classTest.didSave,
),
),
);
on that point it's false, then when the user will complete using the screen, going back with any king of popping the route (with Navigator.pop(context), or with back button...), the dispose method will be called, so you can :
void dispose() {
if(/* your conditions*/ ) {
classTest.didSave = true;
}
super.dispose();
}
it will be back with a true value to the parent page.

Related

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

How to modify current bloc state like adding, updating, and deleting?

I have 2 screens, the first screen is to display a list of items. The second one is a form for creating a new item. On the server side, after each store, update, destroy action, I returns its value back to the client as a response. The goal here is to update the state without having to make a new request over the network and showing a loading screen over and over every time the user creating a new resource and going back to the screen containing the list.
But since the BlocBuilder is only depends on a specific event and state.data is only available inside an if (event is NameOfEvent) statement, I couldn't modify the current state to push the new value to it.
I found that using BlocListener combined with InitState and setState is somehow working, but I think it makes the state.data is useless inside the BlocBuilder also making the algorithm complicated for such a small task.
Is there any simpler way to achieve this condition?
child: BlocBuilder<EducationBloc, EducationState>(
builder: (context, state) {
if (state is EducationLoaded) {
return ListEducationData(educations: state.data);
}
if (state is EducationCreated) {
final education = state.data;
// i want to push [education] to <ListEducationData>
}
...
...
Create list to store data and use BlocListener so you don't need to change ui every state changes.
var _listEducations = List<EducationData>();
child : BlocListener<EducationBloc,EducationState>(
listener: (_,state){
if (state is EducationLoaded) {
setState(() { //update _listEducations
_listEducations.addAll(state.data);
});
}
},
child : Container()
)

How to rebuild the state after pushing a new screen

When i push a new screen onTap with Navigator and pass a new class constructor, how can I have that new screen updates every time _playerTimer updates without having to click again
Since the state of my new class only updates onTap, please help!
The build method of FullScreenDialog is called once since its only being built when onTap is pressed
InkWell(
onTap: () {
return Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FullScreenDialog(
_playerTimer,
));
},
child: VideoPlayer(
_controller,)
);
you have to use setState to rebuild the UI.
ex:
setState((){
_playerTimer = _playerTimer + 1;
});
that's about all the help I can give without seeing the rest of your code
When you instantiate a new class (in your code would be the FullScreenDialog) by passing an attribute, you're only saying to your code that a new class will be initialized by using the argument you provided.
If you want your FullScreenDialog class to always be updated when _playerTimer changes, you must observe this attribute inside this class by using setState(), which is a build-in function for StatefulWidgets that makes apps' UI update every time the observable attribute changes.
Example:
setState( () {
_playerTimer = getUpdatedTimer();
});
Supposing that inside getUpdatedTimer() method you will manage the logic for updating this variable, calling a service or so.
If you want this variable to be updated without interacting with interface, you probably will need a timer, too. Check this question to more details about it.
If you're starting with Flutter development, I suggest you to read this article (Adding interactivity to your Flutter app) about state management
and
setState method documentation.
Hope that helps.

flutter bloc - wrong state is sent from bloc to widget

It seems like bug in bloc v0.11.2
I have the following Event/State:
class DeleteReceipt extends ReceiptEvent {
final Receipt receipt;
DeleteReceipt(this.receipt) : super([receipt]);
}
class ReceiptDeleted extends ReceiptState {
final Receipt receipt;
ReceiptDeleted(this.receipt) : super();
}
and the following code in bloc:
if (event is DeleteReceipt) {
var delReceipt = event.receipt;
await _receiptDao.delete(delReceipt);
print("deleting: " + delReceipt.snapshot.documentID);
yield ReceiptDeleted(delReceipt);
}
and my widget I have:
if (state is ReceiptDeleted) {
print("delete: "+state.receipt.snapshot.documentID);
receipts.delete(state.receipt);
}
and when I do: _receiptBloc.dispatch(DeleteReceipt(receipt));
the first time I get:
I/flutter (28196): deleting: AzgAzcn5wRNFVd7NyZqQ
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ
which is correct, but the second time I do _receiptBloc.dispatch(DeleteReceipt(receipt)); on a different receipt, I get:
I/flutter (28196): deleting: d4oUjrGwHX1TvIDr9L2M
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ
You can see that in the second time the DeleteReceipt event was received with the correct value, but the ReceiptDeleted State was received with the wrong value, and then it just get stuck like this, it never fires ReceiptDeleted State with the correct value, only with the first value.
My app is not trivial, and I have set many events and state in the past, and it worked with no issue (except this one, that probably is related flutter bloc state not received)
Basically I let the user create photos of receipt, that are persistent (using bloc/firestore), and I want to let the user delete them, so when the user click on a receipt, it opens in a new screen:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return ReceiptDetailPage(widget.receipt);
},
),
and when the user click on delete, I show a dialog, and delete the receipt if is OK
var result = await showDialog(
context: context,
builder: (BuildContext dialogCtxt) {
// return object of type Dialog
return AlertDialog(
title: new Text(AppLocalizations.of(context).deleteReceiptQuestion),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text(AppLocalizations.of(context).cancel),
onPressed: () {
Navigator.of(dialogCtxt).pop("cancel");
},
),
new FlatButton(
child: new Text(AppLocalizations.of(context).ok),
onPressed: () {
Navigator.of(dialogCtxt).pop("OK");
},
),
],
);
},
);
if (result == 'OK') {
Navigator.of(context).pop();
_receiptBloc.dispatch(DeleteReceipt(receipt));
}
Solution:
add state/event:
class EmptyState extends ReceiptState {}
class EmptyEvent extends ReceiptEvent {}
after receiving the delete state do:
if (state is ReceiptDeleted) {
print("delete: "+state.receipt.snapshot.documentID);
receipts.delete(state.receipt);
_receiptBloc.dispatch(EmptyEvent()); // add this line
}
and add this to your bloc
if (event is EmptyEvent) {
yield EmptyState();
}
This will cause an empty event and state to be fired and will clear the problem
Explain: I noticed that once I fire a State, the block provider will send that state every time I change a screen, which is strange since the app is receiving a Delete State many time. this is not a problem in my case, since the code will try to delete an element that is already delete and will fail quietly:
void delete(Receipt receipt) {
try {
Receipt oldReceipt = receipts.firstWhere(
(r) => r.snapshot.documentID == receipt.snapshot.documentID);
receipts.remove(oldReceipt);
} catch (e) {
print(e);
}
}
NOTE: this seems to happen with all State that the app is firing, not only the Delete state
So I guest that if I will fire an empty event, it will clear the old Delete state, and will somehow fix the issue, and WALLA...
Note that I didn't had to actually listen to the EmptyState any where in my code
MORE INFO:
I realize that although the bloc seems to loose state, also my design is wrong, because the Data Structure should be updated in the bloc, once the event is received and not in the widget, when the state is received (or not received in this case, which cause the bug)
Initially I used bloc with sembast, but then I wanted the data to be sync with the remote DB, so I replaced sembast with firestore.
but that causes the load time to go from nothing, to more than 2 seconds, and that is a problem since in the original design I load all the data from the DB on every update.
So I tried to update the store and the UI seperatly, ie. instead of reading all the data, I keep a List in my widget and update the widget when the state changes - per update/delete state.
That was a problem, since many state were lost (especially when the user click fast - which cause many events/states to fire)
So I guess a correct solution would be to manage the in-memory Data in a separate Service, and update the Data when the Event is received, and then read all data from the Service instead of the store (when possible)

How to delete data from StreamBuilder after reading?

I want to delete data from my stream after I read it.
Basically I want the same system than channel in Go.
So, if I add 5, 3 and 2, my stream contains 5, 3 and 2.
When I start reading, I get 5, and my stream now contains 3 and 2 etc...
Is it possible?
EDIT: Here my problem with some code.
I use a StreamBuilder to receive data. When I change the state, it trigger again my function like if I'd just receive data.
child: StreamBuilder<Tag>(
stream: widget.tagStream,
initialData: Tag(),
builder: (BuildContext context, AsyncSnapshot<Tag> snapshot) {
/// This should be trigger only when I receive data
if (mapController.ready && snapshot.hasData) {
tag = snapshot.data;
mapController.move(
LatLng(tag.position.latitude, tag.position.longitude),
mapController.zoom);
}
return RubberBottomSheet(...);
),
Here some context:
I have a map with icons representing objects. When I click on an icon or if I search the item related on my search bar, a RubberBottomSheet appears to show informations about the object. To do that, I use a StreamBuilder, so I just need to put the object clicked or searched in it to make my rubber appears and fill in. I also need to centrer on my icon to let the user know where is the object. My problem is that when I open or close my keyboard or when I use a setState (for changing the appearance of my search bar for example), it automatically trigger the StreamBuilder like if it receive new data.
Sorry, I should have started here...
The answer of Amine seems to be the best, but I'd like to share my solution too, maybe it'll help some persons.
After I've executed my code, I pass an empty object to my Stream. I just have to verify that my object is not empty before executing my code and everything works like a charm.
builder: (BuildContext context, AsyncSnapshot<Tag> snapshot) {
if (mapController.ready &&
snapshot.hasData &&
snapshot.data.mobile.nid != 0) {
tag = snapshot.data;
... /// My code
widget.tagStream.sink.add(Tag());
}
I've had similar behavior with a StreamBuilder and I couldn't find a solution for days. What I did instead is use a ListView builder that takes data from an InheritedWidget instead.
So basically instead of having to put data into the stream sink, I simply wrap my data setter in the InheritedWidget in a setState() and the ListView rebuilds every time I change the data.
N.B: My StreamBuilder also involved a map, I've always thought it was the one interfering with it but I never got to solve the problem. As in your case, every time I change the state, the stream rebuilds with the same data it had before.
Have similar problem in my app.
Workaroung I found is quite simple - global variable, which by default will do nothing ("false" in this example).
But in method which need to call setState() change this variable value to block next snapshot.data when rebuilding widget (in this example value "true" will block).
Remember to change variable to default value or you won't get new stream updates.
// global variable - doing nothing by default
bool _clear = false;
(...)
/// Add additional condition
if (mapController.ready && snapshot.hasData && !_clear) {
tag = snapshot.data;
mapController.move(
LatLng(tag.position.latitude, tag.position.longitude),
mapController.zoom);
}
/// change variable to default value
_clear = false;
return RubberBottomSheet(...);
),
(...)
// some method calling setState
void _clearMethod() {
_clear = true;
setState(() {});
}