ScaffoldMessenger throws a hero animation error - flutter

I am using the new ScaffoldMessenger to show a snackbar if a user successfully creates a project.
While showing the snackbar, i navigate the app to the dashboard. But as soon as it hits the dashboard There are multiple heroes that share the same tag within a subtree error is thrown.
I am not using any Hero widget in my dashbard and I have one FloatingActionButton but its hero parameter is set to null.
Sample code:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('A SnackBar has been shown.'),
animation: null,
),
);
Navigator.pushReplacementNamed(context, '/dashboard');
Which results in this error:
The following assertion was thrown during a scheduler callback:
There are multiple heroes that share the same tag within a subtree.
Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), each Hero must have a unique non-null tag.
In this case, multiple heroes had the following tag: <SnackBar Hero tag - Text("A SnackBar has been shown.")>
Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), each Hero must have a unique non-null tag.
In this case, multiple heroes had the following tag: <SnackBar Hero tag - Text("A SnackBar has been shown.")>
Here is the subtree for one of the offending heroes: Hero
tag: <SnackBar Hero tag - Text("A SnackBar has been shown.")>
state: _HeroState#7589f
When the exception was thrown, this was the stack
#0 Hero._allHeroesFor.inviteHero.<anonymous closure>
#1 Hero._allHeroesFor.inviteHero
package:flutter/…/widgets/heroes.dart:277
#2 Hero._allHeroesFor.visitor
package:flutter/…/widgets/heroes.dart:296
#3 ComponentElement.visitChildren
package:flutter/…/widgets/framework.dart:4729
#4 Hero._allHeroesFor.visitor
package:flutter/…/widgets/heroes.dart:309
...

I had the same problem. This happens if you have nested Scaffolds. The ScaffoldMessenger wants to send the Snackbar to all Scaffolds. To fix this you need to wrap your Scaffold with a ScaffoldMessenger. This ensures you that only one of your Scaffold receives the Snackbar.
ScaffoldMessenger(
child: Scaffold(
body: ..
),
)

I ran into same problem and fixed it by removing SnackBar before any call to Navigator with ScaffoldMessenger.of(context).removeCurrentSnackBar().
Look like this with your Sample code:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('A SnackBar has been shown.'),
animation: null,
),
);
ScaffoldMessenger.of(context).removeCurrentSnackBar();
Navigator.pushReplacementNamed(context, '/dashboard');
Here's the link that helped me : https://flutter.dev/docs/release/breaking-changes/scaffold-messenger#migration-guide
Hope it'll work for you

I resolved this by having the call run in the next event-loop iteration:
Future.delayed(const Duration(), () =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(...)));

Had the same problem, turns out I had a scaffold widget returning another scaffold in my subtree (whoops)
If so, then your snackbar is being popped on both scaffolds, and then initiating the transition causes the error.

So this is clearly a bug in Flutter.
The best "official" workaround, is to show the snack bar on the next frame:
WidgetsBinding.instance.addPostFrameCallback((_) {
// ... show the culprit SnackBar here.
});
But we can all agree that it shouldn't happen in the first place.

The answer by #GreenFrog assumes that you're handling navigation on your own, in the case you're facing this problem while using the default back button behavior of the Scaffold widget you'll need to wrap your Scaffold in a WillPopScope widget, this basically vetos requests to Navigator, moreover by using WillPopScope.onWillPop you can essentially call ScaffoldMessenger.of(context).removeCurrentSnackBar(); just before the route is popped.
Example:
...
WillPopScope(
onWillPop: () async {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
return true;
},
child: Scaffold(...,
);

Related

showModalBottomSheet and Unhandled Exception: setState() called after dispose() on parent widget

Context:
I have a modal bottom sheet that pops up, upon selection of Camera/Gallery acquires/selects an image XFile and returns it for processing (uploading) done with the help of image_picker.
This is done with a sample line:
ListTile(
onTap: () {
// definition: Future<XFile?> showCamera(IdPhotoOrientation orientation);
showCamera(orientation).then((value) => Navigator.of(context).pop<XFile?>(value));
},
...
),
Picking an image with showModalBottomSheet is done by returning the selected XFile and processing it on a chained function _handleFile(XFile, enum):
return showModalBottomSheet<XFile?>(
context: context,
builder: (context) {
return SingleChildScrollView(
child: ListBody(
children: [
...
ListTile(
onTap: () {
showCamera(orientation).then((value) => Navigator.of(context).pop<XFile?>(value));
},
leading: Icon(Icons.camera),
title: Text("From Camera"),
),
...
],
),
);
},
).then((value) => _handleFile(value, orientation));
What is the problem:
While processing file in _handle(XFile?, int), I need to update the state of the app to show progress bar updates, circular indicators, uploading status, etc.
Future<void> _handleFile(XFile? xfile, int orientation) {
if (xfile == null) {
return Future.value();
}
// store locally with Uploading Status
var imageService = locator<ImageService>();
setState(() { <-------- offending line (ui_partner_registration_id_photos.dart:103:5)
remoteImageStatus[xfile] = UploadStatus.Uploading;
images[orientation] = xfile;
});
// Upload and update result / error
return imageService.uploadIDPhoto(File(xfile.path), orientation).then((value) {
setState(() {
idPhotos[orientation] = value;
remoteImageStatus[xfile] = UploadStatus.Done;
});
print("Uploaded [${xfile.path}]");
}).onError((error, stackTrace) {
print("Error uploading image");
print(stackTrace);
setState(() {
remoteImageStatus[xfile] = UploadStatus.Error;
});
});
}
Why is this a problem?
setState() cannot be called on a stateful widget that is no longer visible/active/in-focus which is now the case for the showModalBottomSheet. That being said, after calling Navigator.pop() this should no longer be the case as the parent stateful widget is now in focus, this is causing my confusion.
(temporary) Solution
A temporary solution (which does not give exactly the desired result) is to add a mounted check as described here with an example here:
if (mounted) {
setState((){
// perform actions
})
}
StackTrace:
[VERBOSE-2:ui_dart_state.cc(199)] Unhandled Exception: setState() called after dispose(): _RegisterIDPhotosState#b75f9(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
#0 State.setState.<anonymous closure> (package:flutter/src/widgets/framework.dart:1052:9)
#1 State.setState (package:flutter/src/widgets/framework.dart:1087:6)
#2 _RegisterIDPhotosState._handleFile (my-awesome-app/viewcontrollers/register/partner/ui_partner_registration_id_photos.dart:103:5)
#3 _RegisterIDPhotosState.pickImageWithModalPopup.<anonymous closure> (package:my-awesome-app/viewcontrollers/register/partner/ui_partner_registration_id_photos.dart:188:23)
#4 _rootRunUnary (dart:async/zone.dart:1362:47)
#5 _CustomZone.runUnary (dart:async/zone.dart:1265:19)
<asynchronous suspension>
Question:
After selecting a file and starting the upload process, how can I call setState() as in the example of _handleFile(XFile?, int) above?
Refactor that logic to a ChangeNotifier or ValueNotifier higher up in the widget tree and make your Widgets use it to share state between them see the official docs for a more in thorough description.
The setState approach won't work because you are handling 2 different widgets there. You state:
"That being said, after calling Navigator.pop() this should no longer be the case as the parent stateful widget is now in focus, this is causing my confusion."
Whats causing your confusion is that setState is not a global callback which is executed in the currently focused Sateful Widget, setState is nothing more than executing your callback and calling markNeedsBuild for the specific widget in which the setState call was made, which in your case is no longer mounted.
That being said the docs I pointed you to is a recommended way of sharing state in a Flutter app.

Flutter Mobx - setState() or markNeedsBuild() called during build

When I try to navigate from a page to another, I'm getting the error bellow. After some research i found some solutions as calling the navigation inside a SchedulerBinding.instance.addPostFrameCallback, but even if that solve in some cases, for this specific case none of them are working.
I'm using an IndexedStack with three AnimatedOpacity wrapping one different page. Besides the error, could be the way on that the IndexedStack renders its content?
Seems to be cause it renders all the pages at the same time while shows only the current index. But how i can solve this if a i need switch between these pages with the same state? I tried with PageView but the state is miss when change from one page to another.
Anyone can help me on this?
════════ Exception caught by flutter_mobx ══════════════════════════════════════
The following MobXCaughtException was thrown:
setState() or markNeedsBuild() called during build.
This Observer 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:
Observer
The widget which was currently being built when the offending call was made was:
AnimatedOpacity
When the exception was thrown, this was the stack
#0 Element.markNeedsBuild.<anonymous closure>
package:flutter/…/widgets/framework.dart:4167
#1 Element.markNeedsBuild
package:flutter/…/widgets/framework.dart:4182
#2 ObserverElementMixin.invalidate
package:flutter_mobx/src/observer_widget_mixin.dart:70
#3 ReactionImpl._run
package:mobx/…/core/reaction.dart:119
#4 ReactiveContext._runReactionsInternal
package:mobx/…/core/context.dart:345
...
The build method:
#override
Widget build(BuildContext context) {
return IndexedStack(
index: index,
children: <Widget>[
AnimatedOpacity(
key: Key('animatedOpacity_0'),
opacity: index == 0 ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Page1(),
),
AnimatedOpacity(
key: Key('animatedOpacity_1'),
opacity: index == 1 ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Page2(),
),
AnimatedOpacity(
key: Key('animatedOpacity_2'),
opacity: index == 2 ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Page3(),
),
],
);
}
UPDATE:
The problem occurs on the third AnimatedOpacity, and if i remove the AnimatedOpacity widgets, the problems doesn't occurs but i can't figure it out why. Someone has some explanation about that?

flutter - android back button does not call onWillPop of WillPopScope

I would like to exit my application. I have implemented a WillPopScope, but it looks like the onWillPop function is not being called at all. I tried many things like swap WillPopScope with Scaffold, changing the return value of the function, but it just looks like it is not working as expected.
My code:
Future<bool> _willPopCallback() async {
exit(0);
// await showDialog or Show add banners or whatever
// then
return true; // return true if the route to be popped
}
return Scaffold(
appBar: MyAppBar(
leading: DrawerAction(),
title: Text(AppLocalizations.of(context).textCapitalized('home_page')),
onSearch: (searchTerms) => this.search(searchTerms, context),
),
body: new WillPopScope(
onWillPop: _willPopCallback, // Empty Function.
child: //my screen widgets
I am not sure if this is a bug I should report to flutter or I am doing something wrong. Happy to provide more code on request.
I have tried:
exit(0);
Navigator.of(context).pop();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
Thanks in advance!
I managed to solve my problem and I think it is a very particular case, but it still might be helpful to someone.
TL;DR: Ensure that you dont have multiple Scaffolds in your widgets
I was using IndexedStack in my menu navigator, obviously wrapped with a Scaffold. The pages of the stack had Scaffold as well, and with this combination WillPopScope was not working neither in the navigator page neither in its stack pages. I solved by removing all the Scaffolds in the stack pages and having only one in the controller. In this way I managed to use WillPopScope correctly.
First of all do not ever use exit(0). It may be fine in Android environment, but apple won't allow the app on app store if it programmatically shuts down itself.
Here in the docs of onWillPop it clearly mentions that function should resolves to a boolean value.
Future<bool> _willPopCallback() async {
// await showDialog or Show add banners or whatever
// then
return Future.value(true);
}
This only works if your current page is the root of navigation stack.
Modify the code to return WillPopScope, and have Scaffold as a child.
return new WillPopScope(
onWillPop: _willPopCallback,
child: new Scaffold(
//then the rest of your code...
i know i am too late, but the problem still exists.
maybe i found the right solution.
make sure you are passing MaterialApp to the runApp method like this:
runApp(MaterialApp(home: MyFirstPage()));
this works for me for all my application's widgets. if you do not want to use it just wrap your widget in MaterialApp but do not forget that in every MaterialApp instance a new Navigator is created, so for me i just created one as above and in all my pages i just used scaffold and everything is ok.
I also stuck in the same problem but after a lot of searching, I found that this error is related to my parent container.
return WillPopScope(
onWillPop: () => _onWillPop(),
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
...
],
),
Another possible reason: my implementation of _onWillPop() was throwing an exception and the code inside _onWillPop() was ignored. The exception did not appear in the log.
I resolved it by using a TRY/CATCH inside _onWillPop(), and handling all code paths.
I have been battling this and initially thought it had something to do with the nested Scaffold widgets as the OP had mentioned in their answer above. I tested this though and still had the same problem. The answer for me was that my root Scaffold was a child of a Navigator. It worked as soon as I removed the Scaffold as a child of the Navigator. Thankfully I didn't need a Navigator at the root level anyway as I was using an IndexedStack which has multiple Navigator widgets in it.
This is a late answer but I hope can helps someone.
The #Gicminos answer was right. If you have nested scaffold willPopScope not worked.
I wanna add some info in case you need.
I have a Scaffold containing bottomNavBar. Every Item in bottomNav is a Navigator which children are Scaffold (you notice that in this moment there are scaffolds innested).
This is my MainScaffold containing the bottom bar:
...
_navigatorKeys = {
TabItem.tabOne: tabOneKey,
TabItem.tabTwo: GlobalKey<NavigatorState>(),
};
...
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
//check if there are pages in stack so it can pop;
var navigatorState =(_navigatorKeys.values.toList([_selectedIndex]as GlobalKey<NavigatorState>).currentState;
if ( navigatorState !=null) {
if (!await navigatorState
.maybePop()) return true;
}
return false;
},
child: Scaffold(
body: SafeArea(
child: IndexedStack(
index: _selectedIndex,
children: _pages,
),
),
resizeToAvoidBottomInset: true,
bottomNavigationBar: Container(...)
...
}
If you wrap with a WillPopScope widget also your children like in the code below:
#override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context)!;
return WillPopScope(
onWillPop: () async {
debugPrint("test");
return true;
},
child: Scaffold(...)
}
both onWillPop will be called (in main scaffold and children scaffold).
In the example the first one will pop only if can (there are page in navigator stack), the second one will be called immediatly after the first one and it will call the debugPrint function before returned
I my case onWillPop didn't call because I had a custom AppBar and tried to call Navigator.pop(context) instead of Navigator.maybePop(context).

Flutter: how to access context from Dismissible onDismissed

I'm trying to implement undo for a Dismissible list item in Flutter, and having problems accessing a BuildContext.
I have a flutter list, where each item is a card. The card is wrapped in a Dismissible, which allows the user to swipe to dismiss the card. Dismissible automatically removes the item from the list. Dismissible also has an onDismissed event - I'm using this event to update the item in Redux state store (setting an isDismissed flag to true), then show a snackBar which contains an UNDO button.
This is where I'm running into problems. I want the UNDO button to restore the item, by dispatching another action to the Redux store to set isDismissed to false. To do this I need a context, from which to get the store dispatcher. However when I try with the below code, I get an error when clicking on UNDO:
Looking up a deactivated widget's ancestor is unsafe
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard(this.product);
#override
Widget build(BuildContext context) {
return Dismissible(
key: Key(product.id.toString()),
onDismissed: (direction) {
StoreProvider.of<AppState>(context).dispatch(DismissAction(product));
// Then show a snackbar to allow undo
Scaffold.of(context).showSnackBar(
SnackBar(
content: Row(
children: <Widget>[
Expanded(child: Text("Dismissed ${product.title}"),),
FlatButton(
onPressed: () {
// THIS IS WHERE I GET THE ERROR
StoreProvider.of<AppState>(context).dispatch(UndoDismissAction(product));
},
child: Text("UNDO"),
)
],
)
)
);
},
child: Card(
child: ...
)
);
}
}
From what I've read, I think what is going on is that the line StoreProvider.of<AppState>(context) inside the undo button's onPressed action is trying to use a context which belongs to the Card, but because the card has been removed from the list, it no longer exists.
I'm not sure how to do work around this. I've read about flutter keys, and think the answer may be to start passing around some kind of global key, but I can't quite get my head around how that works. I gave it a go and ran into another problem with 'inheritFromWidgetOfExactType' was called on null. Are keys the solution to this problem? If so where do I create the key, do I pass it in to the widget, what type of key should I use etc, or is there a better solution?
Many thanks!
Extract a single copy of the store into a local variable, which will then get captured by all the lambdas below.
#override
Widget build(BuildContext context) {
var store = StoreProvider.of<AppState>(context);
return Dismissible(
...
store.dispatch(DismissAction(product));

Show a snackbar after navigate in Flutter

After an action (for example, "save"), user is returned to another page in the app.
I want to show a snackbar on the new page to confirm the action, but don't want it to show if the user navigated there without the action. So for example, if user saves "thing", the app sends them to "things" main page and shows the "saved" snackbar, but if they go to "things" main page some other way, I don't want the "saved" snackbar to be there.
Here is my code on the panel that saves, but the destination page does not show the snackbar — regardless where I place the snackbar (before or after navigation), it does not execute the snackbar.
Future<Null> saveThatThing() async {
thing.save();
thing = new Thing();
Navigator.of(context).push(getSavedThingRoute());
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text('You saved the thing!'),
),
);
}
What is the best way to do this?
What about if you create key for the screen Scaffold like this (put this object inside the screen state class):
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
and then set this variable to the Scaffold key like this:
return new Scaffold(
key: scaffoldKey,
appBar: new AppBar(
.......,
),
and at the end to show the SnackBar just use somewhere inside the screen state class this code :
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text("Hello")));
or you can wrap the latest code in method like this :
void showInSnackBar(String value) {
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text(value)));}
and then just call this method and pass the snack bar message inside.
If you want to show the snack bar message on the next screen, after you navigate away from the initial context, just call the snack bar method on the next screen, not on the first screen.
Hope this will help
You have to return a result to the previous page which will represent the action shown on the page.
In the 1st page When you are navigating to the page change a few things.
bool result=await Navigator.of(context).push(/*Wherever you want*/);
Then in the second page when you are returning to the previous page send some result to the 1st page.
Navigator.of(context).pop(true);
if the work is not success you can return false.
You can return any type of object as result
Then In the 1st page you can check for the result and show the snackBar accordingly
bool result=await Navigator.of(context).push(/*Wherever you want*/);
if(result!=null && result==true){
//Show SnackBar
}else{
//Some other action if your work is not done
}
Since showing SackBar require an active Scaffold, you will need to pass the message to the new route page something like getSavedThingRoute(message: 'You saved the thing!') and the new page is responsible for displaying the message.
Typically I happen to use Navigation.pop({'message': 'You saved the thing!', 'error': false}) to pass the message.
In your 2nd Page's class, override the initState method
#override
void initState() {
super.initState();
// Shows the SnackBar as soon as this page is opened.
Future(() {
final snackBar = SnackBar(content: Text('Hello World'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
In case anyone using Flushbar plugin. Put this Inside your build function before return statement.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Flushbar(
message: "Message from the top",
flushbarPosition: FlushbarPosition.TOP,
icon: Icon(
Icons.info_outline,
size: 28.0,
color: Color(0xff00d3fe),
),
flushbarStyle: FlushbarStyle.FLOATING,
duration: Duration(seconds: 5),
)..show(_scaffoldKey.currentState.context);
});}
of course don't forget the scaffold key
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
In my case the reason for the snackbar not being shown was the floating action button , I had a logic to put the fab or not and when there was no fab the snack bar did not show , I did not find a proper solution for this but putting a fab like this solved my problem .
however putting Container() , null will cause problems.
Visibility(
visible: false,
child: FloatingActionButton(
onPressed: () {},
),
);