I have a submission form for my app where I have some data the user fills out in a form. I need to GET from an external API in the process, and use that data to create an entry in the database. All this happens once a Submit button is pressed, then after that I want to be able to go back to my homepage route.
I'm not sure how to get data from a Future function without using FutureBuilder, even though I don't need to build a widget, I just need the data.
This is what I have currently:
_populateDB() {
return new FutureBuilder(
future: fetchPost(latitude, longitude),
builder: (context, snapshot) {
if (snapshot.hasData) {
_createJson(snapshot.data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreen()
),
);
} else if (snapshot.hasError) {
return new Text("${snapshot.error}");
}
return new CircularProgressIndicator();
},
);
}
The _populateDB() function is being called when a button is pressed on the screen. What I would like to do is get data from fetchPost(latitude, longitude), use that data in the function _createJson(snapshot.data), and finally go back to the HomeScreen().
I haven't implemented _createJson(snapshot.data) yet, but currently when I call this method with onPressed, it does not go back to the HomeScreen(), and I'm not sure why.
You can get data from a Future function in asynchronous way or in synchronous way.
1 Asynchronous way
It's simple, you can use Native Future API from dart. The method then is a callback method that is called when your Future is completed. You can also use catchError method if your future was completed with some error.
fetchPost(latitude, longitude).then(
(fetchPostResultsData) {
if (fetchPostResultsData != null)
print (fetchPostResultsData);
} ).catchError(
(errorFromFetchPostResults){
print(errorFromFetchPostResults);
}
);
With this Approach your UI isn't blocked waiting results from network.
2 Synchronous way
You can use Dart key words async and await to keep your calls synchronized. In your case you have to transform your _populateDB method in an async method and await from fetchPost results.
_populateDB() async {
var data = await fetchPost(latitude, longitude);
// just execute next lines after fetchPost returns something.
if (data !=null ){
_createJson(snapshot.data);
//... do your things
}
else {
//... your handle way
}
}
With this approach your _populateDB function will wait the results from fetchPost blocking the UI Isolete and just after getting the results will execute the next instructions.
About Navigation if your HomeScreen is the previous the previous widget on stack you just need Navigator.pop(context) call but if there are others widgets in the Stack above of your HomeScreen is a better choice use Navigator.pushReplacement call.
This article shows in details with illustrations how the effects of Navigator methods. I hope it helps.
Use the below code snippet to solve it.
Future.delayed(const Duration(milliseconds: 0))
.then((value) => Navigator.pushNamed(context, '/routeName'));
Related
I'm building a client for a website where users can post comments in a tree-like fashion. Currently, I'm using the following in order to display a loading bar until the comments are loaded.
FutureBuilder(
future: fetchComments(this.storyId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
return LinearProgressIndicator();
case ConnectionState.done:
if (snapshot.hasError) {
final MissingRequiredKeysException myError = snapshot.error;
return Text('Error: ${myError.missingKeys}');
} else {
final api.Comment comment = snapshot.data;
return Expanded(child: Comment(comment.comments));
}
}
}
)
This works pretty well when there are around 200 comments, but when there are more than this the loading bar "hangs" for a noticeable amount of time.
I assume that building the Comment widget takes a significant amount of time since it can be deeply nested.
In order to avoid hanging the main thread, I've modified my code to do the widget creation inside an Isolate:
FutureBuilder(
future: compute<int, Widget>(
buildComments, this.storyId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
return LinearProgressIndicator();
case ConnectionState.done:
if (snapshot.hasError) {
final ArgumentError myError = snapshot.error;
return Text('Error: ${myError.message}');
} else {
final Widget comments = snapshot.data;
return comments;
}
}
},
)
But this is even slower, the UI is blocked for twice the amount of time. I suspect that it might be caused by the data transfer between the isolate and the main isolate (which might happen in the main thread).
What would be a good way to solve this hanging issue?
I would like to make it as transparent as possible for the user (no loading animation when scrolling the list).
I suppose it is working slowly because you load a lot of objects into memory. I suggest you make lazy loading of comments from firebase. First show user only first 20 comments and when he scrolls to the bottom show 20 more comments and so on.
may i suggest that you dont use future builder and get 200 comments per request , as i am sure as you said parsing the data is main reason of hanging as after the async download is finished what happens is you try to parse the data which happens on main queue -not main thread- as flutter is single threaded , so can you show us how you parse the data.
So in my app, I want to make an Ajax request as soon as the widget is mounted, not in initState(). Similar to ComponentWillMount() in react
if the Widget has not mounted then return. Do it before the setState method
if (!mounted) return;
setState(() {});
or
if (mounted) {
//Do something
};
setState(() {});
If you want to execute some code as soon as the widget loaded , you could simply put this code in the initstate like the following ;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => yourFunction(context));
}
In this way , the yourFunction will be exectued as soon as the first frame of the widget loaded on the screen.
I don't think it's currently possible.
Here's the mounted property: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L974
bool get mounted => _element != null;
And here's when _element is set: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L3816
_state._element = this
And I don't see any hook around this code that would inform us.
Why not use initState anyway? It's probably what you want. Here's the comment above the mounted property: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L967
/// After creating a [State] object and before calling [initState], the
/// framework "mounts" the [State] object by associating it with a
/// [BuildContext]. The [State] object remains mounted until the framework
Simply do this as follows.
if (this.mounted) {
setState(() {
//Your code
});
}
I know this answer comes a little bit late but...
Inside your method, you should have something like this:
if(mounted){
setState(() {});
}
This would help, to rebuild the UI only if something changes.
I use this myself inside a method, where I fill my list with users from firestore.
The mounted property helps to avoid the error, when you trying to call setState before build.
I have 2 pages PAGE A and PAGE B. I navigate form PAGE A -> PAGE B and do edit some data, or toggle a setting. Now I want to navigate form PAGE B -> PAGE A and also what that a parameter would be send on navigator pop method. Now my question:
How I can access to these parameter in PAGE A?
Navigator.pop(context, this.selectedEquipmentId);
In fact you got to return something when you ends PageA. I put you an exemple with a popup to select an adress i made recently, this work exactly the same if this is not a popup.
Future<PlacesDetailsResponse> showAdressPicker({#required BuildContext context}) async {
assert(context != null);
return await showDialog<PlacesDetailsResponse>(
context: context,
builder: (BuildContext context) => AdressPickerComponent(),
);
}
You can send a result from Navigator.pop(...) and get it from PageA
Navigator.pop(context, result)
Just put anything you want in result, (here i created a class named PlacesDetailsResponse, use yours or just Int, String...).
Now In pageA when you call this
showAdressPicker(context: context).then((PlacesDetailsResponse value) {
//do whatever you want here
// this fires when PageB call previous to PageA
});
Imagine creating a bottom sheet as follows:
final PersistenBottomSheetController bottomSheetController = showBottomSheet(...);
How do I execute logic upon closing that bottom sheet?
It is a bit unidiomatic for Flutter widgets:
bottomSheetController.closed returns a Future when closing the bottom sheet, which allows for this logic:
bottomSheetController.closed.then((value) {
// this callback will be executed on close
});
Works with await as well:
await bottomSheetController.closed;
// code below this call will get executed upon close
final PersistentBottomSheetController<dynamic> bottomSheetController = scaffoldKey.currentState!.showBottomSheet((context) {
return Container();
});
await bottomSheetController.closed.then((value) {
// the code for working on drawer close
});
My question is about navigation used with the BLoC pattern.
In my LoginScreen widget I have a button that adds an event into the EventSink of the bloc. The bloc calls the API and authenticates the user.
Where in the LoginScreen Widget do I have to listen to the stream, and how do I navigate to another screen after it returns a success status?
Use BlockListener.
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
The navigator is not working in blocBuilder, because in blocBuilder, you can only return a widget
But BlocListener solved it for me.
Add this code:
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
First of all: if there isn't any business logic, then there isn't any need to go to YourBloc class.
But from time to time some user's activity is required to perform some logic in Bloc class and then the Bloc class has to decide what to do next: just rebuild widgets or show dialog or even navigate to next route. In such a case, you have to send some State to UI to finish the action.
Then another problem appears: what shall I do with widgets when Bloc sends State to show a toast?
And this is the main issue with all of this story.
A lot of answers and articles recommend to use flutter_block. This library has BlocBuilder and BlocListener. With those classes you can solve some issues, but not 100% of them.
In my case I used BlocConsumer which manages BlocBuilder and BlocListener and provides a brilliant way to manage states.
From the documentation:
BlocConsumer<BlocA, BlocAState>(
listenWhen: (previous, current) {
// Return true/false to determine whether or not
// to invoke listener with state
},
listener: (context, state) {
// Do stuff here based on BlocA's state
},
buildWhen: (previous, current) {
// Return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// Return widget here based on BlocA's state
}
)
As you can see with BlocConsumer, you can filter states: you can easily define states to rebuild widgets and states to show some popups or navigate to the next screen.
Something like this:
if (state is PhoneLoginCodeSent) {
// Dispatch here to reset PhoneLoginFormState
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return VerifyCodeForm(phoneLoginBloc: _phoneLoginBloc);
},
),
);
return;
});
}