finisinfinitatis
finisinfinitatis

Reputation: 1619

Flutter - Navigate in PopupMenuButton onTap does not work

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,
              ),
            )
          ],
        )
      )
    );
 }

Upvotes: 6

Views: 4977

Answers (4)

Sir Kev
Sir Kev

Reputation: 1

This worked for me. When the menu item is tapped, it schedules a callback to be executed after the current frame is rendered. This ensures that the navigation action (using Get.to(() => Settings());) is performed after the widget tree is fully built, which is necessary for navigation to work correctly.

SchedularBinding.instance!.addPostFrameCallback((_){

//your code here,
//this is executed after the first frame is rendered
});

        

PopupMenuItem(
onTap: (){
    SchedulerBinding.instance!.addPostFrameCallback((_) {
         Get.to(() => Settings());
});

hope it helps:)

Upvotes: 0

Mostafa Alazhariy
Mostafa Alazhariy

Reputation: 359

I also faced this problem and solved it as follows:

  1. First you must remove onTap or make its value null.
  2. 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.
  3. then, pass a function to the onSelected parameter in the PopupMenuButton.
  4. 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);
      }
    },
  ),

Upvotes: 6

Kay
Kay

Reputation: 21

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

Upvotes: 2

Hooshyar
Hooshyar

Reputation: 1270

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

Upvotes: 1

Related Questions