PortalEntry class

A widget that renders its content in a different location of the widget tree.

In short, you can use PortalEntry to show dialogs, tooltips, contextual menus, ... You can then control the visibility of these overlays with a simple setState.

The benefits of using PortalEntry over Overlay/OverlayEntry are multiple:

  • PortalEntry is easier to manipulate
  • It allows aligning your menus/tooltips next to a button easily
  • It combines nicely with state-management solutions and the "state-restoration" framework. For example, combined with RestorableProperty, when the application is killed then re-opened, modals/menus would be restored.

For PortalEntry to work, make sure to insert Portal higher in the widget-tree.

Contextual menu example

In this example, we will see how we can use PortalEntry to show a menu after clicking on a ElevatedButton.

First, we need to create a StatefulWidget that renders our ElevatedButton:

class MenuExample extends StatefulWidget {
  @override
  _MenuExampleState createState() => _MenuExampleState();
}

class _MenuExampleState extends State<MenuExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {},
          child: Text('show menu'),
        ),
      ),
    );
  }
}

Then, we need to insert our PortalEntry in the widget tree.

We want our contextual menu to render right next to our ElevatedButton. As such, our PortalEntry should be the parent of ElevatedButton like so:

Center(
  child: PortalEntry(
    visible: // <todo>
    portal: // <todo>
    child: ElevatedButton(
      ...
    ),
  ),
)

We can pass our menu to PortalEntry:

PortalEntry(
  visible: true,
  portal: Material(
    elevation: 8,
    child: IntrinsicWidth(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(title: Text('option 1')),
          ListTile(title: Text('option 2')),
        ],
      ),
    ),
  ),
  child: RaiseButton(...),
)

At this stage, you may notice two things:

  • our menu is full-screen
  • our menu is always visible (because visible is true)

Let's fix the full-screen issue first and change our code so that our menu renders on the right of our ElevatedButton.

To align our menu around our button, we can specify the childAnchor and portalAnchor parameters:

PortalEntry(
  visible: true,
  portalAnchor: Alignment.topLeft,
  childAnchor: Alignment.topRight,
  portal: Material(...),
  child: RaiseButton(...),
)

What this code means is, this will align the top-left of our menu with the top-right or the ElevatedButton. With this, our menu is no-longer full-screen and is now located to the right of our button.

Finally, we can update our code such that the menu show only when clicking on the button.

To do that, we need to declare a new boolean inside our StatefulWidget, that says whether the menu is open or not:

class _MenuExampleState extends State<MenuExample> {
  bool isMenuOpen = false;
  ...
}

We then pass this isMenuOpen variable to our PortalEntry:

PortalEntry(
  visible: isMenuOpen,
  ...
)

Then, inside the onPressed callback of our ElevatedButton, we can update this isMenuOpen variable:

ElevatedButton(
  onPressed: () {
    setState(() {
      isMenuOpen = true;
    });
  },
  child: Text('show menu'),
),

One final step is to close the menu when the user clicks randomly outside of the menu.

This can be implemented with a second PortalEntry combined with GestureDetector like so:

Center(
  child: PortalEntry(
    visible: isMenuOpen,
    portal: GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        setState(() {
          isMenuOpen = false;
        });
      },
    ),
    child: PortalEntry(
      // our previous PortalEntry
      portal: Material(...)
      child: ElevatedButton(...),
    ),
  ),
)
Inheritance

Constructors

PortalEntry({Key? key, bool visible = true, Alignment? childAnchor, Alignment? portalAnchor, Widget? portal, Duration? closeDuration, required Widget child})
const

Properties

child Widget
final
childAnchor Alignment?
final
closeDuration Duration?
final
hashCode int
The hash code for this object.
no setterinherited
key Key?
Controls how one widget replaces another widget in the tree.
finalinherited
portal Widget?
final
portalAnchor Alignment?
final
runtimeType Type
A representation of the runtime type of the object.
no setterinherited
visible bool
final

Methods

createElement() StatefulElement
Creates a StatefulElement to manage this widget's location in the tree.
inherited
createState() → _PortalEntryState
Creates the mutable state for this widget at a given location in the tree.
override
debugDescribeChildren() List<DiagnosticsNode>
Returns a list of DiagnosticsNode objects describing this node's children.
inherited
debugFillProperties(DiagnosticPropertiesBuilder properties) → void
Add additional properties associated with the node.
override
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) DiagnosticsNode
Returns a debug representation of the object that is used by debugging tools and by DiagnosticsNode.toStringDeep.
inherited
toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) String
A string representation of this object.
inherited
toStringDeep({String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug}) String
Returns a string representation of this node and its descendants.
inherited
toStringShallow({String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) String
Returns a one-line detailed description of the object.
inherited
toStringShort() String
A short, textual description of this widget.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited