PortalTarget class

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

Its child is rendered in the tree as you would expect, but its portalFollower is rendered through the ancestor Portal in a different location of the widget tree.

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

The benefits of using PortalTarget/PortalFollower over Overlay/OverlayEntry are multiple:

  • PortalTarget 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 PortalTarget 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 PortalTarget 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 PortalTarget in the widget tree.

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

Center(
  child: PortalTarget(
    visible: // <todo>
    portalFollower: // <todo>
    child: ElevatedButton(
      ...
    ),
  ),
)

We can pass our menu as the portalFollower to PortalTarget:

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

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 anchor parameter:

PortalEntry(
  visible: true,
  anchor: const Aligned(
    follower: Alignment.topLeft,
    target: Alignment.topRight,
  ),
  portalFollower: Material(...),
  child: ElevatedButton(...),
)

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:

PortalTarget(
  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 PortalTarget combined with GestureDetector like so:

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

Constructors

PortalTarget({Key? key, bool visible = true, Anchor anchor = const Filled(), Duration? closeDuration, PortalFollower? portalFollower, List<PortalLabel> portalCandidateLabels = const [PortalLabel.main], String? debugName, StackFit fit = StackFit.loose, required Widget child})
const

Properties

anchor Anchor
final
child Widget
final
closeDuration Duration?
final
debugName String?
final
fit StackFit
final
hashCode int
The hash code for this object.
no setterinherited
key Key?
Controls how one widget replaces another widget in the tree.
finalinherited
portalCandidateLabels List<PortalLabel>
final
portalFollower → PortalFollower?
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() → _PortalTargetState
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

Static Methods

debugResolvePortal(BuildContext context, List<PortalLabel> portalCandidateLabels) String?
See which Portal will indeed be used given the configuration Visible only for debugging purpose.