Skip to content

Overlay 2.0 / Declarative OverlayEntry #50961

@rrousselGit

Description

@rrousselGit

Related to the discussions on #45938
This can be considered as a request to merge flutter_portal on Flutter

TL;DR, OverlayEntry suffers from the same problems that motivate the work on Navigator 2.0. flutter_portal includes a better list of the problem/solution, but in short:

OverlayEntry is:

  • confusing because it requires using context
  • imperative – which is very unusual since everything else in Flutter is declarative
  • difficult to keep in sync with a state shared between the overlay and the main content
  • difficult to display as a response to a state change as the tree is usually locked when the change is detected (didChangeDependencies used to work, but no longer do on master)

Proposal

Make OverlayEntry declarative / merge flutter_portal into Flutter.

More concretely, instead of having OverlayEntry as a simple Dart class, then manipulated with functions like OverlayState.insert or OverlayEntry.remove, then OverlayEntry would be a Widget.

Since it would be a widget, the entry would be returned inside the build method of a Stateless/StatefulWidget.
Then, instead of an imperative API, the OverlayEntry would be shown based on the life-cycles of the widget:

  • inserting the entry in the widget tree would be equivalent to the current OverlayState.insert
  • removing the widget from the widget tree would be equivalent to OverlayEntry.remove
  • rebuilding the widget would be equivalent to OverlayEntry.markNeedsBuild

Such widget would also have a visible parameter, to be able to show/hide the entry without changing the depth of the widget tree.

NOTE:
Such a refactor would also imply a refactor of the existing showXX methods like showDialog or showSnackbar to make them declarative too.

Ideally, the existing methods would stay available to help people migrate.
May I suggest an equivalent to the new Router but for Overlay+OverlayEntry (Portal? 😛)

Examples

Showing a modal based on a widget parameter

Starting with a complex example of what this kind of feature solves.

This example reproduces the "clap" button of Medium, which shows an overlay that renders the number of times the user clapped.

This is currently not easily feasible. The difficulties are:

  • the overlay needs to be updated based on external factors, which makes it difficult to keep it in sync
    We may have to rely on addPostFrameCallback inside build.
  • the overlay needs to be aligned on the top of the button.
  • we need to determine whether we should create a new OverlayEntry or update an existing one.
    This is not necessarily difficult but complexifies the code.

With a declarative OverlayEntry, it would be a lot simpler.
A setState on the main widget would update both the main content and the overlay at the same time.
It also becomes very easy to manage whether to show the overlay or not.

Here's a fully working code based on flutter_portal:

ezgif com-crop

class ClapButton extends StatefulWidget {
  ClapButton({Key key}) : super(key: key);

  @override
  _ClapButtonState createState() => _ClapButtonState();
}

class _ClapButtonState extends State<ClapButton> {
  int clapCount = 0;
  bool hasClappedRecently = false;
  Timer resetHasClappedRecentlyTimer;

  @override
  Widget build(BuildContext context) {
    return PortalEntry(
      visible: hasClappedRecently,
      // aligns the top-center of `child` with the bottom-center of `portal`
      childAnchor: Alignment.topCenter,
      portalAnchor: Alignment.bottomCenter,
      portal: Material(
        elevation: 8,
        borderRadius: BorderRadius.circular(40),
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text('$clapCount'),
        ),
      ),
      child: RaisedButton(
        onPressed: _clap,
        child: Icon(Icons.plus_one),
      ),
    );
  }

  void _clap() {
    resetHasClappedRecentlyTimer?.cancel();

    resetHasClappedRecentlyTimer = Timer(
      const Duration(seconds: 2),
      () => setState(() => hasClappedRecently = false),
    );

    setState(() {
      hasClappedRecently = true;
      clapCount++;
    });
  }
}

A simple modal

Before, using showDialog and the imperative style

class Before extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
        onPressed: () {
          showDialog<void>(
            context: context,
            builder: (context) {
              return Dialog(
                child: Text('Hello world'),
              );
            },
          );
        },
        child: Text('show dialog'),
      ),
    );
  }
}

After, using a declarative style (where DialogEntry is a widget that internally uses OverlayEntry + adds things like a barrier)

class After extends StatefulWidget {
  @override
  _AfterState createState() => _AfterState();
}

class _AfterState extends State<After> {
  bool _showDialog = false;

  @override
  Widget build(BuildContext context) {
    return DialogEntry(
      visible: _showDialog,
      onClose: () => setState(() => _showDialog = false),
      dialog: Dialog(
        child: Text('Hello world'),
      ),
      child: Center(
        child: RaisedButton(
          onPressed: () => setState(() => _showDialog = true),
          child: Text('show dialog'),
        ),
      ),
    );
  }
}

That example is more verbose, indeed. Although it could be reduced by offering an "uncontrolled" mode similar to TextField where DialogEntry manages its own state but doesn't offer a visible/onClose parameters.

A potential API for such "uncontrolled" mode could look like:

class After2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DialogEntry.uncontrolled(
      dialog: Dialog(
        child: Text('Hello world'),
      ),
      // `open` is a function that shows the dialog
      // We could offer a `close` too.
      builder: (context, open) {
        return Center(
          child: RaisedButton(
            onPressed: () => open(),
            child: Text('show dialog'),
          ),
        );
      },
    );
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: proposalA detailed proposal for a change to Fluttercustomer: crowdAffects or could affect many people, though not necessarily a specific customer.f: routesNavigator, Router, and related APIs.frameworkflutter/packages/flutter repository. See also f: labels.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions