setState() or markNeedsBuild() called during build when usnig Get.toNamed() inside FutureBuilder - flutter

Using flutter 2.x and Get package version ^4.1.2.
i have a widget like so:
class InitializationScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future:
// this is just for testing purposes
Future.delayed(const Duration(seconds: 4)).then((value) => "done"),
builder: (context, snapshot) {
if (shouldProceed(snapshot)) {
Get.toNamed("/login");
}
if (snapshot.hasError) {
// handle error case ...
}
return const Center(child: CircularProgressIndicator());
},
),
);
}
bool shouldProceed(AsyncSnapshot snapshot) =>
snapshot.hasData && snapshot.connectionState == ConnectionState.done;
}
Get.toNamed("/login"); used inside FutureBuilder leads to this error:
The following assertion was thrown building FutureBuilder(dirty, state: _FutureBuilderState#b510d):
setState() or markNeedsBuild() called during build.
I tried to check the connectionStatus (based on a SO answer) but it didn't work.
any help?

build method is intended for rendering UI. You logic is not connected to rendering at all, so even without an error it doesn't make sense to put it into build method.
It's better to convert this method to StatefulWidget and put the logic in initState, e.g.:
Future.delayed(const Duration(seconds: 4))
.then((value) => "done")
.then((_) => Get.toNamed("/login"));

try this
Future.delayed(Duration.zero, () async {
your set state
});

This error happens when the setState() is called immediately during the screen's initial build(). In order to avoid this, you can do a work around by putting the setState() within a Future.delayed, something like:
Future.delayed(Duration(milliseconds: 100), () => Get.toNamed("/login"));

Related

Flutter - FutureBuilder fires twice on hot reload

In my flutter project when I start the project in the simulator everything works fine and the future builder only fires once, but when I do hot reload the FutureBuilder fires twice which causes an error any idea how to fix this?
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
I solved the issue. I put the Future function in the initState and then used the variable in the FutureBuilder. I'm not sure why it works this way, but here's the code:
var futures;
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
futures = frameFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futures,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
The solution as you already figured out is to move the future loading process to the initState of a StatefulWidget, but I'll explain the why it happens:
You were calling your future inside your build method like this:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
The issue is that Flutter calls the build method each time it renders the Widget, whenever a dependency changes(InheritedWidget, setState) or Flutter decides to rebuild it. So each time you redraw your UI frameFuture() gets called, this makes your build method to have side effects (this async call) which it should not, and is encouraged for widgets not to have side effects.
By moving the async computation to the initState you're only calling it once and then accessing the cached variable futures from your state.
As a plus here is an excerpt of the docs of the FutureBuilder class
"The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted."
Hope this makes clear the Why of the solution.
This can happen even when the Future is called from initState. The prior solution I was using felt ugly.
The cleanest solution is to use AsyncMemoizer which effectively just checks if a function is run before
import 'package:async/async.dart';
class SampleWid extends StatefulWidget {
const SampleWid({Key? key}) : super(key: key);
final AsyncMemoizer asyncResults = AsyncMemoizer();
#override
_SampleWidState createState() => _SampleWidState();
}
class _SampleWidState extends State<SampleWid> {
#override
void initState() {
super.initState();
_getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.asyncResults.future,
builder: (context, snapshot) {
if (!snapshot.hasData) return yourLoadingAnimation();
// ... Do things with the data!
});
}
// The async and await here aren't necessary.
_getData() async () {
await widget.asyncResults.runOnce(() => yourApiCall());
}
}
Surprisingly, there's no .reset() method. It seems like the best way to forcibly rerun it is to override it with a new AsyncMemoizer(). You could do that easily like this
_getData() async ({bool reload = false}) {
if (reload) widget.asyncResults = AsyncMemoizer();
await widget.asyncResults.runOnce(() => yourApiCall());
}

how to call setState inside FutureBuilder in Flutter

I'm trying to set State based on result received by FutureBuilder, looks like it is not possible since FutureBuild is still building, any ideas please ? error :
The following assertion was thrown building FutureBuilder<String>(dirty, state: _FutureBuilderState<String>#db9f1):
setState() or markNeedsBuild() called during build.
This StatefulBuilder 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: StatefulBuilder
my code :
FutureBuilder<String>(
future: fetchIdPlayer(idPlayer, bouquet),
builder: (context, snapid) {
if (!snapid.hasData)
return Container(
height: mobileHeight * 0.05,
child: Center(
child: CircularProgressIndicator(),
),
);
else if (snapid.data == "Error_ID") {
setState(() {
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
});
}
})
You can workaround the error you are getting by scheduling the setState to be executed in the next frame and not potentially during build.
else if (snapid.data == "Error_ID") {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
///This schedules the callback to be executed in the next frame
/// thus avoiding calling setState during build
setState(() {
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
});
});
...
You can just wrap the widget who will use resultName and have_ID with FutureBuilder .So there is no need to setState.you can also handle error as well .If you want to setState then use a asyn function and after result is fetched you can just call setState
You could use the property connectionState of snapid.
This should generally work as connectionState is set to ConnectionState.done whenever the future is terminated.
FutureBuilder<String>(
future: fetchIdPlayer(idPlayer, bouquet),
builder: (context, snapid) {
if (snapshot.connectionState == ConnectionState.done) {
setState((){
//...
})
}
if (!snapid.hasData)
//...
else if (snapid.data == "Error_ID") {
//...
}
})
The key bit of this error message is, "called during build". You need to wait until after build. This can be accomplished with either:
WidgetsBinding.instance?.addPostFrameCallback()
https://api.flutter.dev/flutter/widgets/WidgetsBinding-mixin.html
or
SchedulerBinding.instance?.addPostFrameCallback()
https://api.flutter.dev/flutter/scheduler/SchedulerBinding-mixin.html
Flutter prohibit this kind of situation because setting a state in side a future widget will cause to rebuild the future widget again and this will become an infinite loop. Try to separate logic from UI. Example credits goes to #pskink and #Mofidul Islam
In your state class create a wrapper
Future<String> wrapper(idPlayer, bouquet) async {
final foo = await fetchIdPlayer(idPlayer, bouquet);
//handle errors
if(foo.error){
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
}
return foo;
}
call the wrapper in your widget
FutureBuilder<String>(
future: wrapper(idPlayer, bouquet),
builder: (context, snapid) {
if (!snapid.hasData)
return Container(
height: mobileHeight * 0.05,
child: Center(
child: CircularProgressIndicator(),
),
);
else {
//UI to show error
}
})

FutureBuilder - setState() or markNeedsBuild() called during build error

I have the below FutureBuilder entry.
FutureBuilder(
future: _checkConn,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState){
case ConnectionState.none:
Navigator.pushReplacementNamed(context, NoConnView);
break;
case ConnectionState.active:
case ConnectionState.waiting:
case ConnectionState.done:
if(snapshot.data=='OK'){
initialfbget();
break;
} else {
Navigator.pushReplacementNamed(context, NoConnView);
break;
}
}
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.fill,
child: SizedBox(
width: _vcontroller.value.size?.width ?? (MediaQuery.of(context).size.width),
height: _vcontroller.value.size?.height ?? (MediaQuery.of(context).size.height),
child: VideoPlayer(_vcontroller),
),
),
);
}
),
The below is the complete initstate section:
void initState() {
super.initState ();
_vcontroller = VideoPlayerController.asset("assets/testimages/sukiitestanimation.mp4")
..initialize().then((_) {
// Once the video has been loaded we play the video and set looping to true.
_vcontroller.play();
_vcontroller.setLooping(false);
// Ensure the first frame is shown after the video is initialized.
});
_checkConn = checkConn();
Firebase.initializeApp();
}
Below is the checkconn segment:
Future<String> checkConn() async {
var connresponse = await http.get(connurl).timeout(const Duration(seconds: 10));
log('connresponse is: ${connresponse.statusCode}');
if(connresponse.statusCode!=200) {
return "BAD";
} else {
return "OK";
}
}
Kept on receiving the below error.
setState() or markNeedsBuild() called during build.
Would appreciate any assistance on this.
Thanks in advance.
Actually, I don't know what is _checkConn = checkConn(); in your initState() used for, because you've declare your checkConn() at your futureBuilder.
I just can suggest you to solve your problem with these 3 option that you can pick bellow:
1. Remove your await in initState
just delete your _checkConn = checkConn();
2. Remove your setState() during your build Widget
from my experience, the error like I write below can happen when you call setState() during the building widget.
setState() or markNeedsBuild() called during build.
so, I assume that you have some setState() inside your build without any event.
3. Use if mounted
await syntax can make some error when you build it in initState(), you can use 1 of some StatefulWidget() lifecycle to build it, that is mounted.

Show Dialog In InitState Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe

At first time screen appears, i want check if user GPS is enable and Location Permission is granted. Then if one of them is not fulfilled , i show dialog to open app setting.
Source Code
_initPermission(BuildContext context) async {
final geolocationStatus = await commonF.getGeolocationPermission();
final gpsStatus = await commonF.getGPSService();
if (geolocationStatus != GeolocationStatus.granted) {
showDialog(
context: context,
builder: (ctx) {
return commonF.showPermissionLocation(ctx);
},
);
} else if (!gpsStatus) {
showDialog(
context: context,
builder: (ctx) {
return commonF.showPermissionGPS(ctx);
},
);
}
}
Then i called this function in initState like this :
InitState
void initState() {
super.initState();
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission(context));
}
The problem is , every first time the screen appears it will give me error like this :
Error
[ERROR:flutter/lib/ui/ui_dart_state.cc(166)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
What i have done :
Change InitState like this
//1
WidgetsBinding.instance.addPostFrameCallback((_) {
_initPermission(context);
});
//2
SchedulerBinding.instance.addPostFrameCallback((_) => _initPermission(context));
//3
Timer.run(() {
_initPermission(context);
})
Adding Global Key For Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: _scaffoldKey,
Searching similiar problem with me
Show Dialog In Initstate
Looking up a deactivated widget's ancestor is unsafe
Load Data In Initstate
The results that I have done is nothing , the error still appear in first time screen appear.
But strangely, this only happens when first time screen appear . When i do Hot Restart the error message is gone.
[Failed if screen first time appear]
[Hot restart , error gone]
I have no way to test it (I dont have the GPS package) but try changing
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission(context));
to
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission()); //this inside the initstate
_initPermission() async{
final geolocationStatus = await commonF.getGeolocationPermission();
final gpsStatus = await commonF.getGPSService();
if (geolocationStatus != GeolocationStatus.granted) {
await showDialog(
context: context,
builder: (ctx) => commonF.showPermissionLocation
},
);
} else if (!gpsStatus) {
await showDialog(
context: context,
builder: (ctx) => commonF.showPermissionGPS
},
);
}
} // the stateful widget can use the context in all its methods without passing it as a parameter
The error dissapears in hot restart because it just refresh the state of the widget, but it's already created (If you do hot restart and print something inside the initState, for example print('This is init');, you wont see it either because the refresh doesn't dispose and init the widget, so it won't run that piece of code again)
EDIT
Based on your gist I just made a minimal reproducible example and run with no problem in DartPad, later I will try it on VS but for now can you check if there is anything different
enum GeolocationStatus{granted, denied} //this is just for example
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
showPermissionGPS() {
return AlertDialog(
title: Text('GPSStatus'),
);
}
_showPermissionLocation() {
return AlertDialog(
title: Text('Permission Localization'),
);
}
#override
initState(){
super.initState();
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission());
}
_initPermission() async{
final geolocationStatus = await Future<GeolocationStatus>.value(GeolocationStatus.granted); //mocked future with a response to test
final gpsStatus = await Future<bool>.value(false); //mocked future with a response to test
if (geolocationStatus != GeolocationStatus.granted) {
await showDialog(
context: context,
builder: (ctx) => _showPermissionLocation() //this mock commonF.showPermissionLocation and returns an AlertDialog
);
} else if (!gpsStatus) {
await showDialog(
context: context,
builder: (ctx) => _showPermissionGPS() //this mock commonF.showPermissionGPS and returns an AlertDialog
);
}
}
#override
Widget build(BuildContext context) {
return Text('My text');
}
}

Navigator.pop from a FutureBuilder

I have a first screen which ask the user to enter to input, then when the users clicks on a button, the app goes on a second screen which uses a FutureBuilder to call an API.
If the API returns an error, I would like to go back to the previous screen with Navigator.pop. When I try to do that in the builder of the FutureBuilder, I get an error because I modify the tree while I am building it...
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
What is the proper way to go to the previous screen if an error occur?
class Stackoverflow extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<Flight>(
future: fetchData(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ScreenBody(snapshot.data);
} else if (snapshot.hasError) {
Navigator.pop(context, "an error");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
)
),
);
}
}
PS: I tried to use addPostFrameCallback and use the Navigator.pop inside, but for some unknown reason, it is called multiple times
You can not directly navigate when build method is running, so it better to show some error screen and give use chance to go back to last screen.
However if you want to do so then you can use following statement to do so.
Future.microtask(() => Navigator.pop(context));
I'd prefer to convert class into StateFullWidget and get rid of FutureBuilder
class Stackoverflow extends StatefulWidget {
#override
_StackoverflowState createState() => _StackoverflowState();
}
class _StackoverflowState extends State<Stackoverflow> {
Flight flight;
#override
void initState() {
super.initState();
fetchData().then((data) {
setState(() {
flight = data;
});
}).catchError((e) {
Navigator.pop(context, "an error");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: flight != null ? ScreenBody(flight) : CircularProgressIndicator(),
),
);
}
}
and of cause pass context somewhere outside class is not good approach