Popups in Flutter the Right Way
Effortlessly create custom Flutter popups, from dropdowns to dialogs, using only the SDK. Achieve perfectly aligned designs without extra libraries.
Common challenges
Positioning pop-ups is a complex task, with factors such as device rotation and window resizing adding to the challenge. Also, sometimes, the position of the popup needs to be updated frequently (e.g. sliders).
Popups are often implemented using overlays. Managing them directly adds complexity due to their separate lifecycle and limited access to a parent's context because of higher placement in the tree.
OverlayEntry, an imperative API, requires hands-on management of its lifecycle through 'insert' and 'delete' methods. It can outlive its 'parent' widget, which is often higher in the overlay structure. It also lacks access to the 'parent' context, as it is not its direct child.
final overlayEntry = OverlayEntry(
// Potentially, a *root* context that doesn't
// have an access to InheritedWidgets.
builder: (BuildContext context) {
return const Text('Hello, World!');
},
);
// Insert the overlay using Overlay.of(context).insert().
Overlay.of(context).insert(overlayEntry);
// Remove the overlay using overlayEntry.remove().
overlayEntry.remove();
Recommended Solution
OverlayPortal
is a modern successor for OverlayEntry
. It provides declarative show and hide APIs. The rest of Overlay management is handled for us behind the scenes. See what docs tell us about OverlayPortal
:
A widget that renders its overlay child on an Overlay.
The overlay child is initially hidden until OverlayPortalController.show is called on the associated controller. The OverlayPortal uses overlayChildBuilder to build its overlay child and renders it on the specified Overlay as if it was inserted using an OverlayEntry, while it can depend on the same set of InheritedWidgets (such as Theme) that this widget can depend on.
To effectively position elements like a dropdown, we utilize the CompositedTransformTarget
and CompositedTransformFollower
widgets. This approach automatically aligns the follower widget with specified "anchors," ensuring precise placement.
There is a widget named "Popup" that efficiently incorporates the above-mentioned elements. Here's the code defining it:
The "Popup" widget utilizes four key components: the follower, target, anchors, and a controller for managing the overlay's visibility.
Here's how it works:
- Overlay Management: The
OverlayPortal
uses a controller to toggle the follower's visibility. - Linking Elements: A
CompositedTransformTarget
, wrapped around the OverlayPortal, employs a 'link'. This link connects the target and follower. - Positioning the Follower: The
overlayChildBuilder
function generates aCompositedTransformFollower
. This component positions the follower based on specified anchor values.
Align
. Basically, it can be any widget that fills space. If you omit it, your popup will fill the space on the screen.To make the Follower centered under the Target we need to set the followerAnchor to topCenter and targetAnchor to bottomCenter.
Help Container Popup
Let's start from "Help" container and create a popup for it. Let's create a widget:
class _HelpButton extends StatelessWidget {
const _HelpButton(this.controller);
final OverlayPortalController controller;
@override
Widget build(BuildContext context) {
return Popup(
follower: _HelpOverlay(controller.hide),
followerAnchor: Alignment.topRight,
targetAnchor: Alignment.topRight,
controller: controller,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
),
onPressed: controller.show,
child: Row(
children: [
const Icon(
Icons.help_outline,
color: Color(0xFF3F45C4),
),
const SizedBox(width: 4),
Text(
'Help',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontFamily: 'Helvetica',
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
}
This widget takes the OverlayPortalController
and passes it to the popup. It renders the outlined button that shows a follower on tap. The follower is positioned in the upper right corner. The following code defines the successor:
class _HelpOverlay extends StatelessWidget {
const _HelpOverlay(this.hide);
final VoidCallback hide;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200,
child: Card(
margin: EdgeInsets.zero,
surfaceTintColor: Colors.white,
elevation: 4,
shape: const ContinuousRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(28)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...
],
),
),
);
}
}
It takes the hide callback that is invoked when the close button is pressed.
Now the widget is ready to be used in the application, e.g. in the AppBar:
Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text(
'Upgrade Plan',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontFamily: 'Helvetica',
fontWeight: FontWeight.w700,
),
),
actions: [_HelpButton(_infoPopupController)],
),
);
How this works
The Popup widget makes use of OverlayPortal
, which is a more sophisticated alternative to OverlayEntry and provides declarative APIs using the OverlayPortalController
.
The main difference between OverlayEntry
and OverlayPortal
is that the latter builds a child widget of itself, while OverlayEntry
builds its widget subtree as a child of the target Overlay somewhere up in the tree.
CompositedTransformTarget
and CompositedTransformFollower
go to the root of the engine layers. They use fairly simple Proxy Render Objects that apply transformations to actually align the follower to the target.
Popup
widget with controller that exposes show/hide methods.Card Selector Popup
Let's apply the popup widget to create the card selector popup. Again, the target is just a basic text button:
Popup(
follower: _CardSelectorOverlay((value) {
selectedCardChanged(value);
controller.hide();
}),
controller: controller,
followerAnchor: Alignment.bottomLeft,
targetAnchor: Alignment.bottomLeft,
child: SizedBox(
height: 48,
child: Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
label: Text(
selectedCard != null ? '**** **** **** 1234' : 'Select card',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontFamily: 'Helvetica',
fontWeight: FontWeight.w700,
),
),
onPressed: controller.show,
icon: _iconForCard(selectedCard),
),
),
),
);
When button is clicked the popup is shown as well. See how it is implemented in this case:
SizedBox(
width: 250,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(28)),
elevation: 20,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
...
onTap: () => selectedCardChanged('visa'),
),
ListTile(
...
onTap: () => selectedCardChanged('mastercard'),
),
],
),
),
)
Basically, it is a column with two list tiles. It also has some logic with callbacks to return the chosen card.
Final thoughts
The Popup
widget in Flutter leverages OverlayPortal
and CompositedTransform widgets for advanced popup creation, offering a more efficient solution than direct OverlayEntries management.
Please write in the comments how useful this article was to you!