PopupMenuButton opens a menu when user clicks on it. But how do I do open the same menu at the same place as the PopupMenuButton from the code(e.g. due to another user event)?

I am trying to create a nested menu in the AppBar. Like when user clicks on "Sort by" then menu changes to something else. Is there another way to achieve this? I could not find it on the internet.

1 Answers

Smagold On Best Solutions

If you take a look at the sources of PopupMenuButton you can see that it uses showMenu method which is accessible even out of the button's context (potentially comes along with imported material Dart class).

Define a GlobalKey variable and apply it to your PopupMenuButton to have a reference to the context of your button - that's how we get the position of the button on the screen.

Create an array of items for the menu, later to reuse it in both the programmatic call and the physical button itself.

Calculate the position for the menu to appear at, and make the showMenu call.

showMenu returns a Future - listen to its completion to get the chosen item.

// Define these variables somewhere accessible by both the button on `build` & by the programmatic call.

final popupButtonKey = GlobalKey<State>(); // We use `State` because Flutter libs do not export `PopupMenuButton` state specifically.

final List<PopupMenuEntry> menuEntries = [
    value: 1,
    child: Text('One'),
    value: 2,
    child: Text('Two'),

In your build:

  key: popupButtonKey,
  initialValue: null,
  itemBuilder: (_) => menuEntries,
  // ...

To show the menu programmatically:

// Here we get the render object of our physical button, later to get its size & position
final RenderBox popupButtonObject = popupButtonKey.currentContext.findRenderObject();

// Get the render object of the overlay used in `Navigator` / `MaterialApp`, i.e. screen size reference
final RenderBox overlay = Overlay.of(context).context.findRenderObject();

// Calculate the show-up area for the dropdown using button's size & position based on the `overlay` used as the coordinate space.
final RelativeRect position = RelativeRect.fromRect(
    popupButtonObject.localToGlobal(Offset.zero, ancestor: overlay),
    popupButtonObject.localToGlobal(popupButtonObject.size.bottomRight(Offset.zero), ancestor: overlay),
  Offset.zero & overlay.size, // same as: new Rect.fromLTWH(0.0, 0.0, overlay.size.width, overlay.size.height)

// Finally, show the menu.
  context: context,
  elevation: 8.0, // default value
  items: menuEntries,
  initialValue: null,
  position: position,
).then((res) {