Flutter BLoC: Navigator.pop in StreamBuilder in build() method - flutter

I'm following BLoC pattern and subscribing to stream, and reacting to state changes in build method. When data is loaded I want to close the screen.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bloc'),
),
body: SafeArea(
child: StreamBuilder<UserState>(
stream: _userBloc.user,
initialData: UserInitState(),
builder: (context, snapshot) {
if (snapshot.data is UserInitState) {
return _buildInit();
}
if (snapshot.data is UserDataState) {
Navigator.pop(context, true);
return Container();
}
if (snapshot.data is UserLoadingState) {
return _buildLoading();
}
},
),
),
);
}
When I do Navigator.pop(context, true); in build() method I get:
I/flutter ( 4360): ══╡ EXCEPTION CAUGHT BY ANIMATION LIBRARY ╞═════════════════════════════════════════════════════════
I/flutter ( 4360): The following assertion was thrown while notifying status listeners for AnimationController:
I/flutter ( 4360): setState() or markNeedsBuild() called during build.
I/flutter ( 4360): This Overlay widget cannot be marked as needing to build because the framework is already in the
I/flutter ( 4360): process of building widgets. A widget can be marked as needing to be built during the build phase
I/flutter ( 4360): only if one of its ancestors is currently building. This exception is allowed because the framework
I/flutter ( 4360): builds parent widgets before children, which means a dirty descendant will always be built.
I/flutter ( 4360): Otherwise, the framework might not visit this widget during this build phase.
What is the right way to handle such cases in BLoC pattern?
On of the solutions I come up with is to start listening to stream on initState(). In this case I need to broadcast() my stream because I have 2 subscribers.
Are there any better solutions for this?

I think I have got a solution for you. (Please check it)
Make your code look like :
Widget build(BuildContext context) {
// other stuff
if (snapshot.data is UserDataState) {
myCallback(() {
Navigator.pop(context, true);
});
}
// other stuff
}
// after build method (but still in the same class,...) write below method
void myCallback(Function callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}
Hope it helps. Just try it and please report here to help others too!
Source (flutter_bloc Login medium article)
Description

bool hasPop = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bloc'),
),
body: SafeArea(
child: StreamBuilder<UserState>(
stream: _userBloc.user,
initialData: UserInitState(),
builder: (context, snapshot) {
if (snapshot.data is UserInitState) {
return _buildInit();
}
if (snapshot.data is UserDataState) {
if(!hasPop){
hasPop = true;
Navigator.pop(context, true);
}
return Container();
}
if (snapshot.data is UserLoadingState) {
return _buildLoading();
}
},
),
),
);
} ```

I could imagine three possible solutions:
1) It looks to me like it would be best to restructure your widgets. As far as I can see you want a "Loading Screen" .. I see no reason that this has to be it's own navigation item, instead of just another widget.
Ie. You could push the StreamBuilder one? level up.. so your builder method looks like:
if (!snapshot.hasData) {
return LoadingScreen(snapshot);
}
// Whatever you do with your data.
2) I think i personally would create a StatefulWidget, listen to the stream manually in initState() and call setState() manually. No need for a StreamBuilder
3) as a crazy workaround you could probably use Future(() { Navigator.of(context).pop(); }); in your builder. (it's possible that you'd have to use the context of the build method, not the builder. but I wouldn't recommend that solution anyway)

Related

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
}
})

Issue With Nested BlocBuilder() Calls

My Flutter app has multiple BloCs (via the bloc and flutter_bloc packages) which has caused some technical difficulties that I solved using a workaround, but I was wondering if there was a better solution out there.
I am using BlocBuilder() when listening to a bloc where each bloc has its own BlocBuilder() call. At first, I was nesting the BlocBuilder() calls as follows:
Widget build(BuildContext context) {
return BlocBuilder (
bloc: bloc1,
builder: (ctx, snapshot1) {
do_something1(snapshot1);
return BlocBuilder(ctx2, snapshot2) {
bloc: bloc2,
builder: (ctx2, snapshot2) {
do_something2(snapshot2);
return renderWidget();
}
}
}
);
}
The issue I have with this nested BlocBuilder() calls is that if data comes in for bloc1, the BlocBuilder() for bloc2 will be re-called, causing the current state for bloc2 to be re-read() and causing difficulties for do_something2() which ideally should be called only when there is new data for bloc2.
So what I did was to create a separate widget for each BlocBuilder() call, resulting with the following new build():
Widget build(BuildContext context) {
return Column(
children: [
WidgetBlocBuilder1(),
WidgetBlocBuilder2(),
renderWidget(),
],
);
}
What this does is that any data coming in for either bloc1 or bloc2 would be localized in WidgetBlocBuilder1() or WidgetBlocBuilder2() respectively and more importantly, incoming bloc data would NOT cause the BlocBuilder() for the other bloc to be re-called() as was the case in my nested BlocBuilder() approach.
Here is the build() for WidgetBlocBuilder1():

Widget build(BuildContext context) {
return BlocBuilder(
bloc: bloc,
builder: (ctx, snapshot) {
if (snapshot is CommandEditorSaveFile) {
_saveDocumentFile(ctx);
}
return Visibility(
child: Text('test'),
visible: false,
);
},
);
}
Note that WidgetBlocBuilder1() is an invisible widget as shown by the Visibility( visible:false) wrapper. This is because the widget itself should not render anything on the screen; the widget is simply a wrapper for the BlocBuilder() call. If incoming bloc data is supposed to change the visible state of the parent widget, then logic needs to be written to implement that.
This workaround seems to work, but I was wondering if there was a better solution out there.
Any suggestions would be greatly appreciated.
/Joselito
As per a suggestion from pskink, another solution is to use one StreamBuilder() to listen to multiple blocs. To do this, I used a package called multiple_streambuilder.
Here is a sample build() using this package:
Widget build(BuildContext context) {
return StreamBuilder2<int, int>(
streams: Tuple2(bloc1!.stream, bloc2!.stream),
builder: (contex, snapshot) {
if (snapshot.item1.hasData) {
print(snapshot.item1.data);
}
if (snapshot.item2.hasData) {
print(snapshot.item2.data);
}
return Scaffold(
appBar: AppBar(title: Text("test title")),
body: WidgetTest(),
);
},
);
}
In the build() above, I was listening to 2 blocs, both returning an int hence the call to StreamBuilder2<int,int>(). To find out which bloc has emitted data, you call snapshot.item1.hasdata or snapshot.item2.hasdata.

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

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"));

Flutter reuse state from StatefulWidget on rebuild

In short, the question is: How do I reuse the state of an entire widget subtree?
This is what my code currently looks like:
...
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticating) {
return AppLoading();
} else if (state is NotAuthenticated) {
return AppOnboarding();
} else if (state is Authenticated) {
return AppMain();
} else {
return null;
}
}
),
...
Nothing fancy here, just a BlocBuilder rebuilding its child whenever the state of the underlying Bloc changes.
Now, take for instance the following state transitions: NotAuthenticated => Authenticating => NotAuthenticated because there was something wrong with the inputed information. This would result in the AppOnboarding() widget being rebuild completely from scratch with all the information lost. How can I reuse the state from the old AppOnboarding() widget to make it look like the widget was never rebuild?
What I've already tried
I've already tried using a GlobalKey for this which I would pass to the key property of my AppOnboarding widget. This is my code:
_AuthenticatingState extends State<Authenticating> {
GlobalKey<AppOnboardingState> _appOnboardingKey;
#override
initState() {
super.initState();
_appOnboardingKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<...>(
builder: (context, state) {
...
if (state is NotAuthenticated) {
return AppOnboarding(key: _appOnboardingKey);
}
...
}
),
}
}
I was a little surprised this didn't work. Do global keys not maintain the state of the entrie widget-subtree?
Flutter runs at 60 frames per second. The key tells Flutter that some widget is the same one that existed in the last frame. If you remove some widget, even for a single frame, it will dispose the widget (call the dispose method and get rid of the widget).
In other words, the key doesn't "save" any state for later.
You have two options:
Don't dispose the widget at all.
builder: (context, state) {
return Stack(children: [
Offstage(child:AppLoading(), offstage: state is! Authenticating),
Offstage(child:AppOnboarding()), offstage: state is! NotAuthenticated),
Offstage(child:AppMain()), offstage: state is! Authenticated),
]) }
}
Save yourself that widget state, so that you can rebuild the widget later with that same information.

Flutter - Unable to navigate away/show dialog when snapshot error occurs

I got this simple build function which should show a dialog when the snapshot has an error, but I'm unable to do so without getting the following error:
flutter: The following assertion was thrown building
FutureBuilder(dirty, dependencies: flutter:
[_LocalizationsScope-[GlobalKey#f4f3f], _InheritedTheme], state:
flutter: _FutureBuilderState#175f8): 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 phase
flutter: only if one of its ancestors is currently building. This
exception is allowed because the framework flutter: builds parent
widgets before children, which means a dirty descendant will always be
built. flutter: Otherwise, the framework might not visit this widget
during this build phase. flutter: The widget on which setState() or
markNeedsBuild() was called was: flutter:
Overlay-[LabeledGlobalKey#07fd4]
Code:
showUnknownErrorAlertDialog(BuildContext context, [void Function() onClose]) {
showDialog(
context: context,
builder: (BuildContext builderContext) {
return AlertDialog(
title: Text(AppLocalizations.of(builderContext).lang['unknownError']),
actions: <Widget>[
FlatButton(
child: Text(AppLocalizations.of(builderContext).lang['close']),
onPressed: () {
Navigator.of(builderContext).pop();
if (onClose != null) {
onClose();
}
},
),
],
);
}
);
}
goBack() {
locator<AppNavigation>().navigateTo('gameOverview', AppGameArgs(gameName));
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getGameSession(),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return showUnknownErrorAlertDialog(context, () {
goBack();
});
}
/ ... omitted code .../
How am I supposed to handle errors in the build function if I'm not allowed to show a dialog or navigate away instantly when there's an error?
you can't return the dialog directly from the FuturBuilder.You have to use showDialog() to show any dialog and for the click event if you are using navigation in the dialog it'll be used as the navigation of the dialog not the page.
if(snapshot.hasError()){
showDialog(showUnknownErrorAlertDialog(context, () {
Navigator.of(context).pop(1);
})).then((value){
if(value=null && value==1)
goBack();
});
return Container();
}
you can return value from dialog and check the result and run your goBack() there