I have async function in a Flutter widget that will perform these steps:
Future<void> someFunction(BuildContext context) async {
// step 1:
// call async function that will result in removing current widget from the tree.
await foo();
// at this point the context has no widget since the widget is removed from the tree
// step 2:
// need to show a dialog which requires a context with a widget.
// since the context has no widget, I am unable to show the dialog
// What can we do here??
showDialog(context: context, .....);
}
Can someone suggest a solution.
Thanks
Related
I have a function that call showModalBottomSheet which I need to remove the barrier.
Future<..> popup() async => await showModalBottomSheet<..>(
...
builder : (BuildContext context){
...
Navigator.of(context).pop(..);
...
}
);
I have two options. Since I don't want to use different Scaffold, that leaves me to use showBottomSheet instead. But I also want to keep my function as async because I need the BottomSheet to return a value on it's dismiss. showBottomSheet returns PersistentBottomSheetController not a Future. I have no idea how to implement a Future returning function from it, the way I did with showModalBottomSheet.
Future<..> popup() async{
???
Scaffold.of(context).showBottomSheet(..)<..>(
(BuildContext context){
...
Navigator.of(context).pop(..);
...
}
);
???
}
Greatly appreciate the help. Thank you.
You can't return Future from showModalBottomSheet instead
you can use isClosed to check if bottom sheet closed and this getter return future.
you can use it like:
Future<bool> popup(BuildContext context) async{
PersistentBottomSheetController myModal = showBottomSheet(
context: context,
builder: (context) {
return const Text("Modal");
},
);
Future<bool> isClosed = await myModal.closed;
return isClosed;
}
I'm writing a flutter application using flutter 2.10 and I'm debugging it using an Android Emulator
I included the flutter_native_splash plugin from https://pub.dev/packages/flutter_native_splash and I use version 2.0.1+1
the problem that I'm having is that I decide what's the first screen that the user will see based on the initialization phase. I check the stored user token, see his premissions, verify them with the server, and forward him to him relevant route.
since the runApp() function executes in the background while the initialization phase is running I cannot choose the page that will be shown. and if I try to nativgate to a route in the initialization function I get an exception.
as a workaround for now I created an init_home route with FutureBuilder that awaits for a global variable called GeneralService.defaultRoute to be set and then changes the route.
class _InitHomeState extends State<InitHome> {
#override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: () async {
var waitCount=0;
while (GeneralService.defaultRoute == "") {
waitCount++;
await Future.delayed(const Duration(milliseconds: 100));
if (waitCount>20) {
break;
}
}
if (GeneralService.defaultRoute == "") {
return Future.error("initialization failed");
}
Navigator.of(context).pushReplacementNamed(GeneralService.defaultRoute);
...
any ideas how to resolve this issue properly ?
I use a Stateful widget as Splash Screen.
In the build method, you just return the 'loading' view such as Container with a background color etc. (with texts or whatever you like but just consider it as the loading screen).
In the initState(), you call a function that we can name redirect(). This should be an async function that performs the queries/checks and at the end, calls the Navigator.of(context).pushReplacementNamed etc.
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
redirect();
}
#override
Widget build(BuildContext context) {
return Container(color: Colors.blue);
}
Future<void> redirect() async {
var name = 'LOGIN';
... // make db calls, checks etc
Navigator.of(context).pushReplacementNamed(name);
}
}
Your build function just creates the loading UI, and the redirect function in the initState is the one running in the background and when it has finished computing, calls the Navigator.push to your desired page.
I have a reuable stateful widget that returns a button layout. The button text changes to a loading spinner when the network call is in progress and back to text when network request is completed.
I can pass a parameter showSpinner from outside the widget, but that requires to call setState outside of the widget, what leads to rebuilding of other widgets.
So I need to call setState from inside the button widget.
I am also passing a callback as a parameter into the button widget. Is there any way to isolate the spinner change state setting to inside of such a widget, so that it still is reusable?
The simplest and most concise solution does not require an additional library. Just use a ValueNotifier and a ValueListenableBuilder. This will also allow you to make the reusable button widget stateless and only rebuild the button's child (loading indicator/text).
In the buttons' parent instantiate the isLoading ValueNotifier and pass to your button widget's constructor.
final isLoading = ValueNotifier(false);
Then in your button widget, use a ValueListenableBuilder.
// disable the button while waiting for the network request
onPressed: isLoading.value
? null
: () async {
// updating the state is super easy!!
isLoading.value = true;
// TODO: make network request here
isLoading.value = false;
},
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, value, child) {
if (value) {
return CircularProgressIndicator();
} else {
return Text('Load Data');
}
},
);
}
You can use StreamBuilder to solve this problem.
First, we need to create a stream. Create a new file to store it, we'll name it banana_stream.dart, for example ;).
class BananaStream{
final _streamController = StreamController<bool>();
Stream<bool> get stream => _streamController.stream;
void dispose(){
_streamController.close();
}
void add(bool isLoading){
_streamController.sink.add(isLoading);
}
}
To access this, you should use Provider, so add a Provider as parent of the Widget that contain your reusable button.
Provider<BananaStream>(
create: (context) => BananaStream(),
dispose: (context, bloc) => bloc.dispose(),
child: YourWidget(),
),
Then add the StreamBuilder to your button widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: Provider.of<BananaStream>(context, listen:false),
initialData: false,
builder: (context, snapshot){
final isLoading = snapshot.data;
if(isLoading == false){
return YourButtonWithNoSpinner();
} else{
return YourButtonWithSpinner();
}
}
);
}
}
And to change isLoading outside, you can use this code:
final provider = Provider.of<BananaStream>(context, listen:false);
provider.add(true); //here is where you change the isLoading value
That's it!
Alternatively, you can use ValueNotifier or ChangeNotifier but i find it hard to implement.
I found the perfect solution for this and it is using the bloc pattern. With this package https://pub.dev/packages/flutter_bloc
The idea is that you create a BLOC or a CUBIT class. Cubit is just a simplified version of BLOC. (BLOC = business logic component).
Then you use the bloc class with BlocBuilder that streams out a Widget depending on what input you pass into it. And that leads to rebuilding only the needed button widget and not the all tree.
simplified examples in the flutter counter app:
// input is done like this
onPressed: () {
context.read<CounterCubit>().decrement();
}
// the widget that builds a widget depending on input
_counterTextBuilder() {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state.counterValue < 0){
return Text("negative value!",);
} else if (state.counterValue < 5){
return Text("OK: ${state.counterValue}",
);
} else {
return ElevatedButton(onPressed: (){}, child: const Text("RESET NOW!!!"));
}
},
);
}
I'm having an hard time trying to figure out how to update a piece of a view based on a child view "event"... let me explain:
I have a screen which is composed by a Scaffold, having as a body a custom widget which calls a rest api to get the data to display (it makes use of a FutureBuilder), then it dispatch a Notification (which basically wraps the Flutter's AsynchSnapshot) that should be used in order to update the floatingActionButton (at least in my mind :P).
This is the build method of the screen:
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onMyNotification,
child: Scaffold(
appBar: MyAppBar(),
body: MyRemoteObjectView(),
floatingActionButton: MyFloatingButton(),
),
);
}
The view is rendered perfectly, the data is retrieved from the server and displayed by MyRemoteObjectView and the notification is successfully dispatched and received, BUT as soon as I call setState() in my callback, I get the exception:
setState() or markNeedsBuild() called during build.
This is the callback (defined in the same class of the build method above):
bool onMyNotification(MyNotification notification) {
AsyncSnapshot snapshot = notification.snapshot;
if (snapshot.connectionState == ConnectionState.done) {
setState(() {
// these flags are used to customize the appearance and behavior of the floating button
_serverHasBeenCalled = true;
_modelDataRetrieved = snapshot.hasData;
});
}
return true;
}
This is the point in which I send the notification (build method of MyRemoteObjectView's state):
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
MyNotification(snapshot).dispatch(context);
// ...
The point is: how and when should I tell Flutter to redraw the floating button (and/or other widgets)? (because of course without the setState I don't have the exception but the button is not refreshed)
Am I getting the whole thing wrong? Is there an easier way to do it? Let me know
After FutureBuilder is built, it waits for future to return a value. After it is complete, you're calling setState and then FutureBuilder would be built again and so on, resulting in infinite repaint loop.
Are you sure that you need FutureBuilder and NotificationListener in this case? You should probably do it in initState of your StatefulWidget like this:
#override
void initState() {
super.initState();
getData().then((data) {
setState(() {
_serverHasBeenCalled = true;
_modelDataRetrieved = true;
});
});
}
You can also store Future in a state and pass it to FutureBuilder.
I have a listView in my screen. I have attached a controller to it. I am able to call my Endpoint, receive response, parse it and insert in row. ListView supposed to Scroll automatically. It does, but not in perfect way. I am always an item behind. This is my code:
#override
Widget build(BuildContext context) {
// Scroll to the most recent item
if (equationList.length > 0) {
_toEnd();
}
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: EquList(equationList, _scrollController),
floatingActionButton: new FloatingActionButton(
onPressed: onFabClick,
tooltip: 'Fetch Post',
child: new Icon(isLoading ? Icons.pause : Icons.play_arrow),
),
);
}
void _toEnd() {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
The problem is, I am calling _toEnd() function before the last item inserts in to the list. So, I am looking for a callback (if there is any) that tells me build() is done. Then I call my _toEnd() function.
What is the best practice in this case?
General solution
Just to clear things up, I did not expect this question to attract so much attention. Hence, I only answered for this very specific case.
As explained in another answer WidgetsBinding offers a way to add a one time post frame callback.
WidgetsBinding.instance!.addPostFrameCallback((_) {
// executes after build
})
As this callback will only be called a single time, you will want to add it every time you build:
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback((_) => afterBuild);
return Container(); // widget tree
}
void afterBuild() {
// executes after build is done
}
Specific (async)
Elaborating on Günter's comment:
#override
Widget build(BuildContext context) {
executeAfterBuild();
return Container();
}
Future<void> executeAfterBuild() async {
// this code will get executed after the build method
// because of the way async functions are scheduled
}
There is a nice example illustrating that effect here.
Extensive information about scheduling in Dart can be found here.
The async way from #creativecreatorormaybenot is enough to answer the question for most situations.
But if you want to setState() or do something that will change widgets in the tree right after building the widget, you cannot use the async way. Because the callback will be fired during the build process of the widget tree. It will throw an exception:
Dart Error: Unhandled exception:
E/flutter (25259): setState() or markNeedsBuild() called during build.
For this situation, you can register a post frame callback to modify the widget:
#override
Widget build(BuildContext context) {
WidgetsBinding.instance
.addPostFrameCallback((_) => executeAfterWholeBuildProcess(context));
If you don't want to use WidgetsBinding:
Use Future or Timer
#override
Widget build(BuildContext context) {
Timer(_runsAfterBuild); // <-- Just add this line
return Container();
}
Future<void> _runsAfterBuild() async {
// This code runs after build ...
}
Add a dummy wait (fixes #creativecreatureormaybenot problem)
#override
Widget build(BuildContext context) {
_runsAfterBuild();
return Container();
}
Future<void> _runsAfterBuild() async {
await Future.delayed(Duration.zero); // <-- Add a 0 dummy waiting time
// This code runs after build ...
}