Using Navigator inside setState - flutter

I have a list of Strings (called questions).
I create a Text widget based on the current string in the list.
I have an index int that increases every time a button is pressed.
I increase the current index by 1 in the setState method.
I need to navigate to a different page when the current index reaches the length of the String list.
Otherwise, I will get an RangeError naturally.
setState(() {
this.currentIndex++;
if(this.currentIndex == questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
});
Now based on the code above, the error page appears and disappears quickly.
It is replaced by the Loser() page quickly.
Why is that?
And how can I navigate to the Loser() page without the error page showing?
Edit: As requested, the error message:
════════ Exception caught by widgets library ═══════════════════════════════════
The following RangeError was thrown building LandingPage(dirty, state: _LandingPageState#a8efe):
RangeError (index): Invalid value: Not in inclusive range 0..10: 11
The relevant error-causing widget was
LandingPage
lib/main.dart:21
When the exception was thrown, this was the stack
#0 List.[] (dart:core-patch/growable_array.dart:153:60)
#1 _LandingPageState.build
package:testing_http_package/landing_page.dart:88
#2 StatefulElement.build
package:flutter/…/widgets/framework.dart:4628
#3 ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4511
#4 StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:4684
...
════════════════════════════════════════════════════════════════════════════════
Edit: The widget I think in the build method that is causing the error:
child: Center(
child: Text(
questions[currentIndex], // This line
style: style,
textAlign: TextAlign.center,
),
),
),
Shouldn't the setState method go straight to the page before rerunning the build method?
Edit: I added the didChangeDependencies method as per #Nuts suggestion but it did not work. Now only the error page appears and it does not proceeds to the other page:
#override
void didChangeDependencies() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(this.currentIndex == questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
});
super.didChangeDependencies();
}

With setState - you are rebuilding the whole widget and while doing it - navigating. So you are trying to rebuild widgets with invalid params (in your case index)
this.currentIndex++;
if(this.currentIndex => questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
else setState(() {}); // if currentIndex is valid, just rebuild

Related

flutter/dart: Looking up a deactivated widget's ancestor is unsafe

I have used persistent_bottom_nav_bar 5.0.2 package and from the first tab page which is homepage I navigate to further some pages and than finally I use the following navigator to come back to this page on same tab
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
(_) => false,
);
I Navigate successfully but when I click on this tab it throw this exception
════════ Exception caught by gesture ═══════════════════════════════════════════
The following assertion was thrown while handling a gesture:
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.
I even don't know the reason as well as solution so any help will be appreciated.
You are removing all the screens below HomePage from the stack.
pushAndRemoveUntil as name suggests pushes a new Screen in stack and removes all the other screens from stack as a result theirs no screen available to pop except the current HomePage.
Instead use only
if (mounted) {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
(_) => false,
);
}

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.

ScaffoldMessenger throws a hero animation error

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(...,
);

Looking up a deactivated widget's ancestor is unsafe using provider and snackbar

I'm using DismissibleWidget to remove a note from a list. When the note is removed a Snackbar is called to give the chance for the user to undo the action. When the button is clicked, and the note is reinserted in the list, this error appears:
The following assertion was thrown while handling a gesture:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
The dismissible widget
Dismissible(
direction: DismissDirection.endToStart,
onDismissed: (direction){
var removedNote = list.notes[index];
Provider.of<ListProvider>(context, listen: false).removeNote(index);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text("Nota removida"),
action: SnackBarAction(
label: "Desfazer",
onPressed: (){
Provider.of<ListProvider>(context, listen: false).insertNote(removedNote, index);
},
),
duration: Duration(seconds: 3),
),
);
},
...
The method in the provider
void insertNote(NoteModel note, int index){
notes.insert(index, note);
fileRepository.saveToFile(notes);
notifyListeners();
}
I faced the some problem but with BLoC as sate management, my solution was taking the context from above that widget.
What went wrong?
You were trying to look for a context that is no longer available. This ocurrs also because of variable shadowing.
Solution?
Reach the content above that that widget.
Pseudo Flutter Code
class NameOfYourClass{
build(BuildContext context1) {
return ListView.builder(
itemBuilder: (context2, index) {
return MyItem();
}
);
}
}
Let's say you dismiss the MyItem with the Dismissible widget, the context2 will be no longer available, if you print the context2 it will look something like this(NOTE: When MyItem is Dismissed):
SliverList(delegate: SliverChildBuilderDelegate#66afb(estimated child count: 1), renderObject: RenderSliverList#593ed relayoutBoundary=up2 DETACHED)
As you can see, the item is DETACHED from the tree. If you print the context1 it will look something like this.
NameOfYourClass
So in conclusion, instead of doing this:
Provider.of<ListProvider>(context2, listen: false).insertNote(removedNote, index);
Do this:
Provider.of<ListProvider>(context1, listen: false).insertNote(removedNote, index);
Don't use context1 or context2, this is for illustration purposes.

This ValueListenableBuilder widget cannot be marked as needing to build

in simple part of my application i defined this value as ValueNotifier:
final ValueNotifier<List<MediaDropDownStructure>> _mediaFoldersList = ValueNotifier<List<MediaDropDownStructure>>([]);
i used this variable inside DropDownBottom items to fill them and create manu and i fill that by this code inside StreamBuilder:
StreamBuilder<List<MediaModel>>(
stream: _globalBloc.storageMediaBloc.imagesMedia$,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator( ),
);
}
final List<MediaModel> _allImages = snapshot.data;
_mediaFoldersList.value = _allImages.map( (image) => MediaDropDownStructure( image.folder, image.folder ) ).toList();
final MediaModel _all = _allImages[0];
return GridView.builder(
...
and i use that inside DropDownBotton like with:
child: ValueListenableBuilder(
valueListenable: _mediaFoldersList,
builder: (context, List<MediaDropDownStructure> items,child)=>DropdownButtonHideUnderline(
child: DropdownButton<MediaDropDownStructure>(
value: _chooseFolderName.value,
hint: Text("please choose",style: AppTheme.of(context).caption(),),
items: items.map((MediaDropDownStructure menuItem) {
return DropdownMenuItem<MediaDropDownStructure>(
value: menuItem,
child: Text(menuItem.folderPath),
);
}).toList(),
onChanged: (_) {},
),
),
),
and i get this error:
The following assertion was thrown while dispatching notifications for ValueNotifier<List<MediaDropDownStructure>>:
setState() or markNeedsBuild() called during build.
This ValueListenableBuilder<List<MediaDropDownStructure>> 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: ValueListenableBuilder<List<MediaDropDownStructure>>
dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#2b62a]]
state: _ValueListenableBuilderState<List<MediaDropDownStructure>>#6c134
The widget which was currently being built when the offending call was made was: StreamBuilder<List<MediaModel>>
dirty
state: _StreamBuilderBaseState<List<MediaModel>, AsyncSnapshot<List<MediaModel>>>#cbf2d
problem is that while streambuilder is building its state meanwhile _mediaFoldersList's value also change, so it will also start to build ValueListenableBuilder and it is creating issue because two builder can not build together.
To solve it you can change _mediaFoldersList's value after 1 microsecond, so streambuilder builder complete it's build method and then ValueListenableBuilder can build.
cratemethod like below.
changevaluenotifiervalue(_allImages) async {
await Future.delayed(Duration(microseconds: 1));
_mediaFoldersList.value = _allImages.map( (image) => MediaDropDownStructure( image.folder, image.folder ) ).toList();
}
call this method where you are changing its value.
final List<MediaModel> _allImages = snapshot.data;
//_mediaFoldersList.value = _allImages.map( (image) => MediaDropDownStructure( image.folder, image.folder ) ).toList(); //commented
changevaluenotifiervalue(_allImages); // added
final MediaModel _all = _allImages[0];