skip to content

In Flutter, how do Overlay with OverlayEntry and OverlayPortal differ when floating a photo-options popover above the current route?

level: middleimportance: should knowfreq 35%

answer

  1. the Navigator's Stack of entries
  2. insert, markNeedsBuild, remove, dispose
  3. built under the Overlay's context
  4. controller show and hide
  5. overlayChildBuilder shares inherited widgets

basics

~20 s

An OverlayEntry is inserted into an Overlay by hand, built under the Overlay's context, and lives until removed. OverlayPortal builds its floating child inside its own subtree, so it shares that subtree's inherited widgets and cannot outlive it.

solid answer

~50 s

The `Overlay` is a Stack-like widget the `Navigator` creates; each route is an entry in it. With `OverlayEntry(builder: ...)` you call `Overlay.of(context).insert(entry)`, `markNeedsBuild()` when its content changes, and later `remove()` then `dispose()`. The builder runs as a child of the `Overlay`, so it sees inherited widgets above the Overlay, not ones placed inside your route. If you forget `remove()`, the popover stays on screen after the page pops. `OverlayPortal` (since 3.10) takes an `OverlayPortalController` (`show`, `hide`, `toggle`, `isShowing`) and an `overlayChildBuilder`. Its overlay child is painted on the nearest `Overlay` by default but belongs to the portal's subtree, so it shares the portal's `Theme` and providers, and it cannot outlive the portal. For a popover anchored to the camera button, `OverlayPortal` is the better default; `OverlayEntry` suits content that should not belong to any one widget.

code

dart · 43 lines
dart
import 'package:flutter/material.dart';

class CameraButton extends StatefulWidget {
  const CameraButton({super.key});

  @override
  State<CameraButton> createState() => _CameraButtonState();
}

class _CameraButtonState extends State<CameraButton> {
  final OverlayPortalController _menu = OverlayPortalController();
  final LayerLink _link = LayerLink();

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: _link,
      child: OverlayPortal(
        controller: _menu,
        overlayChildBuilder: (BuildContext context) => CompositedTransformFollower(
          link: _link,
          targetAnchor: Alignment.bottomRight,
          child: Align(
            alignment: AlignmentDirectional.topStart,
            child: Card(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextButton(onPressed: _menu.hide, child: const Text('Take photo')),
                  TextButton(onPressed: _menu.hide, child: const Text('Choose from library')),
                ],
              ),
            ),
          ),
        ),
        child: IconButton.filled(
          onPressed: _menu.toggle,
          icon: const Icon(Icons.photo_camera),
        ),
      ),
    );
  }
}

go deeper

for a junior

Recall that the Overlay floats content above the page, that entries are inserted and removed by hand, and that OverlayPortal shows content with a controller.

for a middle

Explain where each API builds its content, which inherited widgets it sees, and the insert, markNeedsBuild, remove and dispose lifecycle.

for a senior

Choose OverlayPortal for widget-owned popovers and OverlayEntry for app-wide content, anchor with a LayerLink, and prevent leaked overlays after navigation.

for a principal

Set a team pattern for floating UI (portal-based components, one owner for app-wide overlays) so popovers stay themed, accessible and cleaned up consistently.

## The Overlay An `Overlay` is a widget that holds a list of **overlay entries** and lays them out with a Stack layout: later entries paint on top of earlier ones. The `Navigator` that `MaterialApp`, `CupertinoApp` and `WidgetsApp` create contains an `Overlay`, and every route is one or more entries in it. Content inserted there floats above the page, outside the page's own layout, which is why popovers, drag previews and toasts use it. There are two APIs for putting your own content into it. ## OverlayEntry: imperative and independent `OverlayEntry` has a required `builder` and three flags, all defaulting to `false`: `opaque`, `maintainState` and `canSizeOverlay`. Its lifecycle is manual: 1. **Create** it with a builder. 2. **Insert** it with `Overlay.of(context).insert(entry)`; pass `rootOverlay: true` to `Overlay.of` to target the app's root overlay. 3. **Rebuild** it with `entry.markNeedsBuild()` when data its builder reads has changed; it does not rebuild with your widget. 4. **Remove** it with `entry.remove()`, which may be called only once. 5. **Dispose** it with `entry.dispose()`, which asserts that it was removed first. Two properties shape how it behaves: - The builder's widgets are **children of the Overlay**, so they see the inherited widgets above the Overlay, such as the app's `Theme`, but not any `Theme`, `DefaultTextStyle` or provider placed inside your route. - The entry lives **until you remove it**. If the page that inserted it is popped and nobody called `remove()`, the popover stays on screen over the next page. ## OverlayPortal: declarative and scoped `OverlayPortal`, added in Flutter 3.10, inverts the relationship. You place it in your widget tree like any widget: - `controller` — an `OverlayPortalController` with `show()`, `hide()`, `toggle()` and `isShowing`. - `overlayChildBuilder` — builds the floating content. - `child` — the normal in-place content, for example the camera button. - `overlayLocation` — `OverlayChildLocation.nearestOverlay` by default, or `rootOverlay`. The overlay child is rendered by the target `Overlay` but is built as part of the portal's own subtree. The framework documentation lists the consequences: - It can **depend on the same inherited widgets** as the portal, so a `DefaultTextStyle` or `Theme` above the camera button styles the popover too. - It is **guaranteed not to outlive** the portal: when the avatar widget is disposed, the popover goes with it. - **Paint order**: it paints after the overlay entry that contains the portal (usually its route) and before the next entry; among several portals, the one that called `show()` last paints on top. - `hide()` removes the overlay child on the next rebuild, so state inside it can be lost. The cost is a more complex implementation, for example extra work during global-key reparenting. The documentation's rule: if the content does not benefit from being part of the portal's subtree, use an `OverlayEntry`. ## Anchoring the popover to the camera button Neither API positions content relative to a widget by itself. Common approaches: 1. `CompositedTransformTarget` around the button and `CompositedTransformFollower` in the overlay child, linked by a `LayerLink`. 2. `OverlayPortal.overlayChildLayoutBuilder`, whose builder receives the child's size and location within the Overlay during layout, so the popover can follow the button and flip near screen edges. | Question | `OverlayEntry` | `OverlayPortal` | |---|---|---| | How shown | `insert` / `remove` | `controller.show()` / `hide()` | | Built under | the Overlay | the portal's own subtree | | Inherited widgets seen | those above the Overlay | those above the portal | | Lifetime | until removed and disposed | bounded by the portal | | Rebuilds | `markNeedsBuild()` | with the portal's subtree | | Good for | app-wide toasts, drag previews | popovers, dropdowns and tooltips tied to one widget | The framework's own `Tooltip` moved to `OverlayPortal` in 3.13, which is a good signal for anchored, widget-owned floating content.

  • In Flutter, why might an OverlayEntry's popover stay on screen after the user navigates back?
    An `OverlayEntry` lives in the Overlay until someone calls `remove()`. Popping the route disposes the widget that inserted it, but not the entry. Call `remove()` and then `dispose()` from the owning State's `dispose()`, or switch to `OverlayPortal`, whose overlay child cannot outlive the portal.
  • When is OverlayEntry still the better choice than OverlayPortal?
    When the floating content does not belong to any single widget's subtree: an app-wide toast queue, a drag preview that outlives its source, or content inserted from a service with only a navigator context. The framework notes that `OverlayPortal` does extra work, for example during global-key reparenting, so plain `OverlayEntry` is simpler when inheriting from a widget adds nothing.

saying these in an interview costs you the question

  • An OverlayEntry's builder sees the inherited widgets of the widget that inserted it.
  • Popping a route automatically removes OverlayEntries it inserted.
  • OverlayPortal.targetsRootOverlay is the current way to target the root Overlay.
  • An OverlayEntry can be disposed without being removed first.
  • OverlayPortal positions its overlay child next to its child automatically.