Use showDialog with conditional in build method - flutter

I have a screen, which shows a button. If I press it, an async job is started. During this time, I want to show an AlertDialog with a spinning wheel. If that job is finished, i will dismiss the dialog or show some errors. Here is my (simplified) code:
Widget build(BuildContext context) {
if (otherClass.isTaskRunning()) {
showDialog( ... ); // Show spinning wheel
}
if (otherClass.hasErrors()) {
showDialog( ...); // Show errors
}
return Scaffold(
...
FlatButton(
onPress: otherClass.startJob
)
);
}
The build will be triggered when the job status is changed or if there are errors. So far, so good, but if I run this code, I got this error message:
Exception has occurred. FlutterError (setState() or markNeedsBuild()
called during build. This Overlay widget cannot be marked as needing
to build because the framework is already in the process of building
widgets. A widget can be marked as needing to be built during the
build phase only if one of its ancestors is currently building. This
exception is allowed because the framework builds parent widgets
before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build
phase. The widget on which setState() or markNeedsBuild() was called
was: Overlay-[LabeledGlobalKey#357d8] The widget which
was currently being built when the offending call was made was:
SettingsScreen)
So, the repaint of the screen will be overlap somehow. I am not sure how to fix this. It feels like I am using this completely wrong. What is the prefered way to handle this kind of interaction (trigger "long" running task, show progress indicator and possible errors)?

The problem is the dialog is going to show while the build method hasn't already finish. So if you want to show a Dialog, you should do it after the build method has finished. To do that, you can use this: WidgetsBinding.instance.addPostFrameCallback(), that will call a function after the last frame was built (just after build method ends).
Other thing you can do is using the ternary operator to show a loading widget like so:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: otherClass.isTaskRunning()
? CircularProgressIndicator()
: FlatButton(
onPressed: () {},
),
);
}

As being said in the comment, calling showDialog and similar inside build is anti-pattern. Here is the detailed explanation.
Although is a bit late, it is important to note showDialog is indeed a async method, returning at the time that the dialog dismisses, and build is a sync in nature. Under the hood, it use Navigator.of(context).push.
Referring to this question,
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as: Route pop/push
So this directly caused flutter to complain setState is called during build. You could just use a FutureBuilder inside the dialog.
One thing is clear is that your SettingsScreen is not required to fetch anything and so you should not brother with any of them. As you are deferring to do that as the button fires, then you should do it within dialog.
Widget build(BuildContext context) {
return Scaffold(
body: FlatButton(
onPress: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return FutureBuilder(
future: otherClass.startJob(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) // Show error
if (snapshot.hasData) // Show data, or you can just close it by Navigator.pop
else // show spinning wheel
}
}
);
}
);
}
)
);
}

Related

Trying to use showDialog()/show Pop up on app startup

What I want to achieve: I want to open a pop up explaining my app when it starts.
My approach: As far as I understand it from googling the issue, I should use the showDialog() method. In its most basic form:
showDialog(
context: context,
builder: (context) {
return Text('data');
});
I tried returning actual dialogs (e.g. AlertDialog) but it doesn't change the behavior so I'm just using Text() with a string as a placeholder for now.
The problem:
No matter where I place the showDialog function, it doesn't work as intended (also see scrennshots below):
Placing it in initState: I get an error message about inherited Widgets being called before the initState is done + an explanation about dependiencies I can barely follow.
Placing it in the build method: I get an error message that setState() or markNeedsBuild() gets called while the app is already buildung widgets.
Placing it in DidChangeAppLifeCycleState(): This is actually working and opening the pop when I pause the app and then resume it. It is not opening on app startup though.
Wrapping it in WidgetsBinding.instance!.addPostFrameCallback(): An idea I picked up here: How to show a popup on app start in Flutter. Doesn't change the outcome of the error messages, neither in initState nor in build.
Any ideas?
Screenshots:
From initState:
From build method:
From DidChangeAppLifecycleState (the "succesful" variant:
Will you please try below code in your init method? I hope this may work.
Future.delayed(Duration.zero, () async {
myFunction();
});
Using WidgetsBinding.instance.addPostFrameCallback inside initState perform its inner task after the 1st frame is complete.
addPostFrameCallback Schedule a callback for the end of this frame.
Next issue arise for not having material. You can directly return AlertDialog on builder or wrap with any material widget like Material, Scaffold..
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(
context: context,
builder: (context) {
return const AlertDialog(
content: Text('data'),
);
},
);
});
}
If you are running version<3 null safety, use WidgetsBinding.instance?.addPostFrameCallback
One of the methods with WidgetsBinding.instance!.addPostFrameCallback() works fine .
If you show a normal show dialog with the press of a button too it will produce the same result.
Here, you need to wrap the text("data") in a dialog widget such as alertDialog or simpleDialog widget as needed and it will display the dialog within the current scaffold as -
WidgetsBinding.instance!.addPostFrameCallback((_) async {
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text("data"),
);
});
});
I tried adding this in the init state and the dialog pops up fine when I restart the app
Thanks a lot for your answers. I ficed the issue by rewriting with your suggestions; and it works. I tihnk the issue was that I did not have _ or anything else in my WidgetsBinding code. So I did:
WidgetsBinding.instance?.addPostFrameCallback(() {})
instead of
WidgetsBinding.instance?.addPostFrameCallback((_) {})

How to Stop the flutter rebuilds from ValueListenableBuilder?

I'm having an issue of the widget subtree constantly rebuilding from inside the ValueListenableBuilder. It is supposed to run a rebuild on change, and in this case it is listening to a table on a Flutter Hive Database.
Things I've tired:
I had all of my Hive Boxes open in the main method, so that I have access to each box from anywhere in the app. I tired only opening the Hive box when something is changed, then promptly closing this box. Didn't work
Things I think it could be, but not sure:
Mixing ChangeNotifierProvider with the ValueListenableBuilder - Because some of the subtree also utilizes changenotifier, but with ValueListenableBuilder constantly rebuilding the subtree, any changes I pass into the provider get wiped out.
Is there anyway of only rebuilding on a change only?
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable:
Hive.box<Manifest>(HiveTables.manifestBox).listenable(),
child: assignmentWidgets,
builder: (context, Box<Manifest> manifestBox, child) {
if (manifestBox.isNotEmpty)
return child!;
},
);
}
the Hive provides listening to a whole Box, so every time something happens inside that Box, the builder will be called :
ValueListenableBuilder<Box>(
valueListenable: Hive.box('settings').listenable(),
builder: (context, box, widget) {
// build widget
},
),
but you can also specify a List<String> of keys that you want only them to be listenable by the ValueListenableBuilder with the keys property like this:
ValueListenableBuilder<Box>(
valueListenable: Hive.box('settings').listenable(keys: ['firstKey', 'secondKey']),
builder: (context, box, widget) {
// build widget
},
),
now for keys other than firstKey and secondKey, every operation that's executed over them will not update the ValueListenableWidget, but operation on firstKey and secondKey will update it only.

FLUTTER : Displaying a loading indicator while listview.builder is building

When clicking on an OPEN LIST button, the app creates a series of lists needed for future display. I found out that when the number of items in the list increases, these calculations can take a little time (2, 3 seconds). So for better UX, I would like to add something similar to a loading indicator telling the user the "lists are being prepared".
In my app, I use the package Loading Indicator : it works fine.
So I wanted to use it for this situation.
Here's what I did :
I transformed my "void" list creating functions into "Future Void".
I added the async keyword to the function plugged to my "OPEN LIST BUTTON".
But... for some reason, it never displays the loading indicator....
Here's the code (UI part) :
onMenuOuvrir: () async {
DialogBuilder(context).showLoadingIndicator(
text: 'Ouverture de la liste', color: Colors.black45);
uD.setSelectedCarnetList(
index, uD.userInfo!.carnetList![index].ref!);
await uD.getListReady();
DialogBuilder(context).hideOpenDialog();
Navigator.pushReplacementNamed(
context, EditCarnetScreen.id);
},
Here's the code (Provider / back end part) :
Future<void> getListReady() async {
await createBufferCarnetWordBank();
await createBufferCarnetList();
await createBufferGrammarList();
await createBufferLevelList();
setEditMode(false);
clearSearchList();
}
The functions "createBuffer...List" are all of type Future .
What am I doing wrong ?
I actually found out that the problem was elsewhere... not in the creation of the lists, but in the building of the "listview.builder" in the EditCarnetScreen.
So here's the question now... how can we display some kind of indicator while this task is being processed.... it seems to be "in between screens"...
Create a widget in a separate dart file :
import 'package:flutter/material.dart';
Loading(BuildContext context) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
);
});
}
Then use it anywhere in your code:
Loading(context)
Dismiss in whenever you want:
Navigator.of(context).pop()
You can use FutureBuilder widget for that, e.g.
FutureBuilder(future: Provider.of<//your provider>(context)getListReady(), builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting ? Center(child: CircularProgressIndicator()) : //your widget)
If you process the calculation in the main thread, even if the indicator shows up the UI freezes and the spinning animation would be laggy.
You may process the heavy lifting tasks by Isolates. Search for Isolate or compute method of dart.
I have used the fluttertoast: ^8.0.8 package as shown below. You can set the toast length to a longer duration if needed. FutureBuilder is not useful in the case, where your widget themselves take a long time render. FutureBuilder is useful while fetching the data required for your widgets.
#override
Widget build(BuildContext context) {
Fluttertoast.showToast(
msg: 'Loading...'
);
//Code for your complex widget here
}

Flutter - How to dynamically add height value of a container before loading the UI?

I have added a setState Method inside the build widget after getting my data from API response via StreamBuilder. But it gives this error:
Unhandled Exception: setState() or markNeedsBuild() called during build.
How do I avoid this situation?
My code:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: StreamBuilder(
stream: bloc.getData,
builder: (context, AsyncSnapshot<Home> dataSnapshot) {
if (dataSnapshot.hasData) {
if (dataSnapshot.data.description != null) _expandHeightBy(40);
........
Function
_expandHeightBy(double increase) async {
setState(() {
_expandedHeightVal += increase;
});
}
It is not possible to call setState during build, and for good reason. setState runs the build method. Therefore if you were able to call setState in build, it would infinitely loop the build method since it essentially calls itself.
Check out this article. It has an example of conditionally rendering based on a StreamBuilder. https://medium.com/#sidky/using-streambuilder-in-flutter-dcc2d89c2eae
Removed the setState from the method calling as #CodePoet suggested and it actually worked fine.
_expandHeightBy(double increase) {
_expandedHeightVal += increase;
}

How to navigate in flutter during widget build?

I'm trying to detect that user is no longer authenticated and redirect user to login. This is how I'm doing it
Widget build(BuildContext context) {
return FutureBuilder(
future: _getData(context),
builder: (context, snapshot) {
try {
if (snapshot.hasError && _isAuthenticationError(snapshot.error)) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginView()));
}
Unfortunately doing navigation on build is not working. It throws this error
flutter: setState() or markNeedsBuild() called during build.
flutter: This Overlay widget cannot be marked as needing to build because the framework is already in the
flutter: process of building widgets. A widget can be marked as needing to be built during the build
I cannot just return LoginView widget since parent widget containts app bar and floating button and login view needs to be displayed without these controlls.. I need to navigate.
Is it possible to do it?
Wrap it in Future.microtask. This will schedule it to happen on the next async task cycle (i.e. after build is complete).
Future.microtask(() => Navigator.push(
context,
MaterialPageRoute(builder: (context) => LoginView())
));
Streams in flutter
The usual thing is to use a flow where user changes occur.
When the user logs off, he detects that change and can direct it to another window.
problem here :
snapshot.hasError && _isAuthenticationError(snapshot.error)
Instead of this use OR
snapshot.hasError || _isAuthenticationError(snapshot.error)