Flutter - Navigate in PopupMenuButton onTap does not work - flutter

I used to use IconButton's onpressed to navigate to the settings page from my AppBar which worked.
Now I am trying to trigger the navigation from a PopupMenuItem's onTap but the page does not navigate. Both widgets are in the same hierarchy and I can't find out the reason for the different behavior. No error is thrown.
Here is the code of my appBar which contains the actions:
Widget build(BuildContext ctx) {
return Container(
child: Scaffold(
appBar: AppBar(
title: const Text('MyApp'),
actions: [
PopupMenuButton(
itemBuilder: (context) => [
// THE NAVIGATION IN onTap DOES NOT WORK
PopupMenuItem(
child: Text(AppLocalizations.of(context).settings),
onTap: () => _openSettings(ctx),
),
],
icon: Icon(
Icons.settings,
),
),
// THIS WORKS
IconButton(onPressed: () => _openSettings(ctx),
icon: Icon(Icons.settings))
],
),
body: Text("")
),
);
}
And here the function whose navigation call only works inside IconButton's onpressed.
I could confirm that the function was triggered in both cases though:
Future<void> _openSettings(BuildContext ctx) async {
print('settings');
await Navigator.push(
ctx, MaterialPageRoute(builder: (ctx) => SettingsPage()));
print('settings completed');
}
I'd appreciate any help!
Workaround:
I have not found out what the issue with onTap navigation is but now I am just using onSelected which results in the same UX and works:
Widget build(BuildContext ctx) {
return Container(
child: Scaffold(
appBar: AppBar(
title: const Text('MyApp'),
actions: [
PopupMenuButton(
onSelected: (result){
switch(result){
case 0: _openSettings(); break;
case 1: _signOut(); break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
child: Text(AppLocalizations.of(context).settings),
value: 0
),
PopupMenuItem(
child: Text(AppLocalizations.of(context).logout),
value: 1
)
],
icon: Icon(
Icons.settings,
),
)
],
)
)
);
}

I had the same issue (except: I am using GetX for Navigation.
I think this happens because flutter is closing the PopupMenuButton automatically and because navigation happens to fast, it closes the new route instead of the menuButton.
I solved it like this in the navigating method:
void edit() async {
await Future.delayed(const Duration(milliseconds: 10));
Get.toNamed('/new/route'); // same like Navigator.of(context).pushNamed()
}
It's not beautiful... i know. But it works XD

I also faced this problem and solved it as follows:
First you must remove onTap or make its value null.
Second, give a value to the value parameter in PopupMenuItem. You can give any type like int or String or whatever you want since value is dynamic.
then, pass a function to the onSelected parameter in the PopupMenuButton.
Inside the onSelected function you can choose what you want to do when any of the menu items is pressed.
In your case, your code should be like this 👇
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Text(...),
value: 'settings',
),
],
onSelected: (value){
if(value == 'settings'){
_openSettings(ctx);
}
},
),

you need to invoke the future function using ()=>, and pass Build Context for the function to know which context to navigate from.
so your button will change like this:
IconButton(onPressed:()=> _openSettings(context), icon: Icon(Icons.settings))
and your function should be like this:
Future<void> _openSettings(BuildContext ctx) async {
print('settings');
await Navigator.push(
ctx, MaterialPageRoute(builder: (context) => SettingPage()));
print('settings completed');
}
here is the entire code that I've tested the issue on:
class PlayGround extends StatefulWidget {
const PlayGround({Key? key}) : super(key: key);
#override
_PlayGroundState createState() => _PlayGroundState();
}
class _PlayGroundState extends State<PlayGround> {
final _title =
TextEditingController.fromValue(TextEditingValue(text: 'initialTitle'));
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MyApp'),
actions: [
PopupMenuButton<int>(
itemBuilder: (context) => [
PopupMenuItem(
child: Text(AppLocalizations.of(context).settings),
onTap: () {},
),
PopupMenuItem(
child: Text(AppLocalizations.of(context).logout),
onTap: () {},
)
],
icon: Icon(
Icons.settings,
),
),
IconButton(
onPressed: () => _openSettings(context),
icon: Icon(Icons.settings))
],
),
body: Container());
}
Future<void> _openSettings(BuildContext ctx) async {
print('settings');
await Navigator.push(
ctx, MaterialPageRoute(builder: (context) => SettingPage()));
print('settings completed');
}
}
class SettingPage extends StatelessWidget {
const SettingPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}

Related

MaterialPageRoute not opening new screen

I am trying to navigate to a new screen when a PopupMenuItem is tapped. I am using showMenu() to create the popup menu.
The code calling the new screen looks like this:-
items: [
PopupMenuItem(
onTap: () {
print(movie.title);
// Navigator.of(context).push(MaterialPageRoute(
// builder: (context) => MoreInfo(movie: movie)));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoreInfo(movie: movie)));
},
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoreInfo(movie: movie)));
},
child: Text("More info")),
and MoreInfo screen code looks like this:-
Scaffold(
backgroundColor: Colors.indigo,
appBar: AppBar(
leading: InkWell(
onTap: () {
Navigator.pop(context);
},
child: Icon(Icons.arrow_back_outlined),
),
title: Text(widget.movie.title),
),
However, using the onTap field of PopupMenuItem does not open a new screen. It is printing out movie.title though. I also have the text of the menu item wrapped with an InkWell, when I do click on the text, a new screen does show up, but there is no content on it. Just an empty screen with Colors.indigo background color as specified in MoreInfo, but with none of the other elements such as AppBar.
What could be going wrong here? Do I need to use another type of Navigation?
this piece of code works as well:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
showMenu(
context: context,
position: const RelativeRect.fromLTRB(0, 0, 0, 0),
items: [
PopupMenuItem(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoreInfo()));
},
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoreInfo()));
},
child: const Text("More info")),
)
]);
},
child: const Text('push me'),
)
],
),
),
);
}
}
class MoreInfo extends StatefulWidget {
MoreInfo({Key? key}) : super(key: key);
#override
State<MoreInfo> createState() => _MoreInfoState();
}
class _MoreInfoState extends State<MoreInfo> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
onTap: () {
Navigator.pop(context);
},
child: const Icon(Icons.arrow_back_outlined),
),
title: const Text('widget.movie.title'),
),
body: const Center(
child: Text('hi'),
),
);
}
}

How to close specific Dialog

I am opening a dialog from another dialog and trying to close the 1st dialog, but it is closing the recent dialog. Similar kind of git issue.
I've tried
putting ValueKey on AlertDialog
using rootNavigator:true while pop
keeping context into variable and doing Navigator.of(specifiqContext).pop()
But none of them is working.
Code to reproduce the issue on dartPad.
class MultiDialogTest extends StatefulWidget {
const MultiDialogTest({Key? key}) : super(key: key);
#override
State<MultiDialogTest> createState() => _MultiDialogTestState();
}
class _MultiDialogTestState extends State<MultiDialogTest> {
BuildContext? dialog1Context, dialog2Context;
Future<void> _showDialog1(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (c) {
dialog1Context = c;
return AlertDialog(
key: const ValueKey("dialog 1"),
title: const Text("Dialog 1"),
content: ElevatedButton(
child: const Text("close dialog2"),
onPressed: () {
if (dialog2Context != null) {
Navigator.of(dialog2Context!,).pop();
}
},
),
actions: [
ElevatedButton(
child: const Text("close this"),
onPressed: () {
Navigator.of(c, rootNavigator: true).pop();
},
),
],
);
});
dialog1Context = null;
}
Future<void> _showDialog2(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (c) {
dialog2Context = c;
return AlertDialog(
key: const ValueKey("dialog 2"),
title: const Text("Dialog 2"),
actions: [
ElevatedButton(
child: const Text("close this"),
onPressed: () {
Navigator.of(c, rootNavigator: true).pop();
},
),
],
content: Column(
children: [
ElevatedButton(
onPressed: () async {
await _showDialog1(context);
},
child: const Text("Open dialog 1"),
),
],
),
);
});
dialog2Context = null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_showDialog2(context);
},
child: const Text("show dialog 2"),
),
),
);
}
}
How can I close bellow Dialog(Dialog 2) without closing above(Dialog 1).
I don't like to close both and reopen the Dialog 1.
You need to pass context of the dialog you want to close (parentContext) and call:
Navigator.pop(parentContext); // close parent
Navigator.pop(context); // close current
Create a separate context and pass the correct context which one you want to close to the Navigator.pop(yourContextThatYouWishToClose)
Navigator.pop(dialogContext);
Here is the example code.
BuildContext dialogContext; // <<----
showDialog(
context: context, // <<----
barrierDismissible: false,
builder: (BuildContext context) {
dialogContext = context;
return Dialog(
child: new Row(
mainAxisSize: MainAxisSize.min,
children: [
new CircularProgressIndicator(),
new Text("Loading"),
],
),
);
},
);
await _longOperation();
Navigator.pop(dialogContext);
What you could do is pop twice in showDialog1 and then await for showDialog1 immediately.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const MultiDialogTest(),
);
}
}
class MultiDialogTest extends StatefulWidget {
const MultiDialogTest({Key? key}) : super(key: key);
#override
State<MultiDialogTest> createState() => _MultiDialogTestState();
}
class _MultiDialogTestState extends State<MultiDialogTest> {
Future<void> _showDialog1(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (c) {
return AlertDialog(
key: const ValueKey("dialog 1"),
title: const Text("Dialog 1"),
content: ElevatedButton(
child: const Text("close dialog2"),
onPressed: () async {
Navigator.of(context).pop();
Navigator.of(context).pop();
await _showDialog1(context);
},
),
actions: [
ElevatedButton(
child: const Text("close this"),
onPressed: () {
Navigator.of(c).pop();
},
),
],
);
});
}
Future<void> _showDialog2(BuildContext context) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (c) {
return AlertDialog(
key: const ValueKey("dialog 2"),
title: const Text("Dialog 2"),
actions: [
ElevatedButton(
child: const Text("close this"),
onPressed: () {
Navigator.of(c).pop();
},
),
],
content: Column(
children: [
ElevatedButton(
onPressed: () async {
await _showDialog1(context);
},
child: const Text("Open dialog 1"),
),
],
),
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_showDialog2(context);
},
child: const Text("show dialog 2"),
),
),
);
}
}
When you use showDialog what happens under the hood is a simple push on the Navigator, this will add the Dialog on the top of the current Navigator as a new Route.
All the pop methods in Navigator simply pop from the topmost route so this is not easily feasible.
A dirty hack may be to pop twice and show again the first dialog like in this sample that works in your dartpad sample
onPressed: () {
if (dialog2Context != null) {
Navigator.of(dialog2Context!).pop();
Navigator.of(dialog2Context!).pop();
_showDialog1(context);
}
},
In my opinion, having a dialog spawning another dialog its not the best UX you can provide to your user, but you can always check which routes are involved by using the inspector:
https://docs.flutter.dev/development/tools/devtools/inspector
in this case, you can quickly check that the dialog will be always on top (in this case the latest of the tree), the proper way to fix this should be to create several navigators and decide which one to use for showing your dialog, but that will complexity a lot of your code!

How to call function from Stateful widget

I have seen many questions similar to mine in Stack Overflow but it did not fit my case since they were asking to call function from - to Stateful widget.
I want call function located into State Full widget from a non Stateful-Stateless Widget
My code is complicated, I will try to explain it below:
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: key);
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
void myFunction(){
print('hello dart');
}
ShowDialog showDialog = ShowDialog();
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: (){
showDialog.myDialog();
},
child: Text('tab me')
);
}
}
class ShowDialog {
Widget myDialog(){
return showDialog(
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
// here i need to call myFunction() Methood
},
child: const Text("tab")
),
],
)
},
);
}
}
How can I go through this?
you can call it directly like this:
_ExampleState().myFunction();
The full code:
class Example extends StatefulWidget {
const Example ({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<Example > createState() => _ExampleState ();
}
class _ExampleState extends State<Example > {
void myFunction(){
print('hello dart');
}
ShowDialog showDialog = ShowDialog();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TextButton(
onPressed: (){
showDialog.myDialog(context);
},
child: Text('tab me')
)
);
}
}
class ShowDialog {
Future myDialog(BuildContext context){
return showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
// here i need to call myFunction() Methood
_ExampleState().myFunction();
},
child: const Text("tab")
),
],
);
},
);
}
}
The result:
So in order to have your showDialog function call MyFunction, you need to pass it as if it was a callback.
To do that, first add a Function callback in your class:
class ShowDialog {
ShowDialog({required this.callback});
VoidCallback callback;
...
}
Then you have to pass the callback when you create the object:
ShowDialog showDialog = ShowDialog(callback: myFunction);
You actually can't do that tho, because this is a class variable, a simple solution is to turn your showDialog variable into a getter, this means showDialog will compute again every time you call it, which is not ideal but I don't think it will be terrible for this specific use-case.
ShowDialog get showDialog => ShowDialog(callback: myFunction);
note the get keyword and the => instead of an equal sign
EDIT:
You can also pass the callback as part of the myDialog method, this is probably actually a better idea:
Widget myDialog(VoidCallback callback) {
return showDialog(
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: callback,
child: const Text("tab")
),
],
)
},
);
}
I wrote this function that returns a Dialog in a separate file :
Future myDialog({required BuildContext dialogContext, required Function function}){
return showDialog(
context: dialogContext,
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
function();
},
child: const Text("tab")
),
],
);
},
);
}
then I made 2 functions, the first one just print (Hello first function) and the second function print (Hello Second function) and set the state and rebuild the widget tree
I made 2 TextButton: the first TextButton call the myDialog function and pass the firstFunction as a parameter, the second TextButton call the myDialog function and pass the secondFunction as a parameter :
the code :
class ExampleState extends State<Example > {
void firstFunction(){
print('Hello first function');
}
void secondFunction(){
setState(() {
print('Hello Second function');
});
}
#override
Widget build(BuildContext context) {
print('build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
TextButton(
onPressed: (){
myDialog(dialogContext: context, function: firstFunction );
},
child: Text('First Dialog')
),
TextButton(
onPressed: (){
myDialog(dialogContext: context, function: secondFunction );
},
child: Text('Second Dialog')
)
],
),
)
);
}
}
notice that I added a print('build'); so I can know when it rebuild the widget tree
The result:

How to change the state of an Action widget in an AlertDialog

I want to validate the user input and then enable the Dialog's Submit action.
Using StatefulBuilder inside the dialog's content doesn't work here, since the actions does not live in the content Widget. So how would one go about changing a property on an action widget?
Currently my workaround is to not use actions, and have a Row of buttons inside the dialog's content, which is OK, but I'd like to know whether there is a better way.
I feel that using a Provider just to hold the "validation result" is a bit overkill.
EDIT: Example demonstrating that a StatefulBuilder allows only inner widgets to be updted with state changes. The button sets enabled, but the of two "action buttons" only the one inside the StatefulBuilder is rebuilt. The AlertDialog actions does not get updated.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Dialogs'),
),
body: Center(
child: TextButton(
child: Text('Clickme'),
onPressed: _dialogWithAction,
),
),
);
}
Future<void> _dialogWithAction() async {
showDialog<bool>(
context: context,
builder: (BuildContext dialogContext) {
bool enabled = false;
return AlertDialog(
title: Text('Add Address'),
content: StatefulBuilder(builder: (context, innerSetState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
child: Text('Enable Submit'),
onPressed: () {
enabled = true;
innerSetState(() {});
},
),
Row(
children: [
// This Workaround widget is rebuild by the stateSetter
TextButton(
onPressed: enabled ? () => Navigator.pop(context) : null,
child: Text('Submit'),
)
],
)
],
);
}),
actions: <Widget>[
// This Widget does not get rebuilt when "enabled" is changed
TextButton(
child: Text('Submit'),
onPressed: enabled
? () {
Navigator.of(dialogContext).pop();
}
: null,
),
],
);
},
);
}
}

SwitchListTile not turning on in a Dialog

I am trying to create a workable switchlist in a Dialog. The Switch with icon, title shows but is not turning on.
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton.icon(
icon: Icon(AntDesign.sound),
label: Text('Sound Options'),
onPressed: () {
setState(() {
showDialog(
context: context,
child: SimpleDialog(
children: <Widget>[
SimpleDialogOption(
child: SwitchListTile(
title: Text('Mute'),
secondary: Icon(AntDesign.sound),
value: _mute,
onChanged: (bool value) {
setState(() {
_mute = value;
});
}),
),
],
));
});
},
)
],
),
),
),
);
}
setState rebuilds your build method and change the _mute value, but the SimpleDialog is open in another context, so that dialog is not rebuild. You should make a new StatefulWidget with the AlertDialog and setState when changing the value there
class Class1 extends StatefulWidget {
#override
_Class1State createState() => _Class1State();
}
class _Class1State extends State<Class1> {
bool _mute = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton.icon(
icon: Icon(AntDesign.sound),
label: Text('Sound Options'),
onPressed: () {
showDialog(
context: context,
builder: (context) => MyDialog(_mute)
);
},
)
],
),
),
),
);
}
}
class MyDialog extends StatefulWidget {
bool mute;
MyDialog(this.mute);
#override
_MyDialogState createState() => _MyDialogState();
}
class _MyDialogState extends State<MyDialog> {
#override
Widget build(BuildContext context) {
return SimpleDialog(
children: <Widget>[
SimpleDialogOption(
child: SwitchListTile(
title: Text('Mute'),
secondary: Icon(AntDesign.sound),
value: widget.mute,
onChanged: (bool value) {
setState(() {
widget.mute = value;
});
}),
),
],
);
}
}
Another few tips, use builder instead of child with showDialog, child is deprecated, don't wrap the whole SimpleDialog in a setState, you should just wrap the value you're going to change, wrapping the dialog won't make it rebuild with the new value.
From the documentation
Calling setState notifies the framework that the internal state of
this object has changed in a way that might impact the user interface
in this subtree, which causes the framework to schedule a build for
this State object.
the setState you use rebuilds the subtree of your whole Scaffold Stateful Widget, but the Dialog is open in another tree, it's not part of that subtree, If you use Android Studio there is a flutter inspector (and I believe Visual Studio has it too), you can see there that the dialog and your StatefulWidget come from Material App (the dialog is not part of the subtree so it won't rebuild)