flutter_portal 0.0.1+2

Build Status pub package codecov

Motivation #

Flutter comes with two classes for manipulating "overlays":

But OverlayEntry is very akward to use. As opposed to most of the framework, OverlayEntry is not a widget (which comes with a nice and clean declarative API).

Instead, is uses an imperative API. This comes with a few drawbacks:

  • a widget's life-cycle (like initState) cannot add/remove synchronously an OverlayEntry.

    This means the first rendering of our entry is effectively one frame late.

  • It is difficult to align an OverlayEntry around a non-overlay widget (for example for a contextual menu).

    We basically have to do everything ourselves, usually needing an addPostFrameCallback which, again, makes the rendering of our overlay one frame late.

  • Overlay.of is hardly customizable and O(N). If we want to add our OverlayEntry on a specific Overlay, we may have to rely on GlobalKey, which is not ideal.

That's where portal comes into play.

This library is effectively a reimplementation of Overlay/OverlayEntry, under the name Portal/PortalEntry (the name that React uses for overlays) while fixing all the previously mentionned issues.

Install #

First, you will need to add portal to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_portal: ^0.0.1

Then, run flutter packages get in your terminal.

Usage #

To use portal, we have to rely on two widgets:

  • Portal, the equivalent of Overlay.

    This widget will need to be inserted above the widget that needs to render under your overlays.

    If you want to display your overlays on the top of everything, a good place to insert that Portal is under MaterialApp but above Navigator, by doing the following:

    MaterialApp(
      builder: (_, child) => Portal(child: child),
    );
    

    (works for CupertinoApp too)

    This way Portal will be above your Navigator so your overlays will properly render above your routes. But it will be a descendant of MaterialApp, so overlays will be able to access Theme/MediaQuery/...

  • PortalEntry is the equivalent of OverlayEntry.

    As opposed to OverlayEntry, using portal then PortalEntry is a widget, and is therefore placed inside your widget tree (so the build method).

    Consider the following OverlayEntry example:

      class Example extends StatefulWidget {
      const Example({Key key, this.title}) : super(key: key);
    
      final String title;
      @override
      _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
      OverlayEntry entry;
    
      @override
      void initState() {
        super.initState();
        entry = OverlayEntry(
          builder: (context) {
            return Text(widget.title);
          },
        );
    
        SchedulerBinding.instance.addPostFrameCallback((_) {
          Overlay.of(context).insert(entry);
        });
      }
    
      @override
      void dispose() {
        SchedulerBinding.instance.addPostFrameCallback((_) {
          entry.remove();
        });
        super.dispose();
      }
    
      @override
      void didUpdateWidget(Example oldWidget) {
        super.didUpdateWidget(oldWidget);
        SchedulerBinding.instance.addPostFrameCallback((_) {
          entry.markNeedsBuild();
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return const Text('whatever');
      }
    }
    

    Using PortalEntry instead, we would write:

    class Example extends StatelessWidget {
      const Example({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return PortalEntry(
          portal: Align(
            alignment: Alignment.topLeft,
            child: Text(title)
          ),
          child: const Text('whatever'),
        );
      }
    }
    

    These two examples are identical in behavior:

    • When mounting our Example widget, an overlay is added, which is later removed when the widget is removed from the tree.
    • the content of that overlay is a Text, that may change over time based on a title variable.

    On the other hand, there's a difference: Using PortalEntry does not rely on addPostFrameCallback.

    As such, inserting/updating our Example widget immediatly inserts/updates the overlay, whereas using OverlayEntry the update is late by one frame.

Aligning the overlay around a widget #

Sometimes, we want to align our overlay around another widget. PortalEntry comes with built-in support for this kind of feature.

For example, consider that we have a Text centered in our app:

Center(
  child: Text('whatever'),
)

If we wanted to add an overlay that is aligned on the top center of our Text, we would write:

Center(
  child: PortalEntry(
    portalAnchor: Alignment.bottomCenter,
    childAnchor: Alignment.topCenter,
    portal: Card(child: Text('portal')),
    child: Text('whatever'),
  ),
)

This will align the top-center of child with the bottom-center of portal, which renders the following:

Target a specific Portal with a PortalEntry

Sometimes you may have multiple Portal, and want to add your PortalEntry on a very specific Portal.

By default, PortalEntry will add its portal on the nearest Portal.

But you can customize this behavior by having subclassing Portal:

mixin NoOp {}
/// Fork [Portal] and all its parameters/properties
class MyPortal = Portal with NoOp; 

Then you can target a custom Portal on PortalEntry by specifying a generic parameter:

MyPortal(
  child: Portal(
    // adds the portal on MyPortal instead of Portal
    child: PortalEntry<MyPortal>(
      ...
    )
  ),
),

0.0.1+2 #

Fix pub badge

0.0.1+1 #

Improve package description

0.0.1 #

Initial implementation

example/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_portal/flutter_portal.dart';

void main() => runApp(MyApp());

// This is a rudimentary tooltip built using `portal`

class MyTooptip extends StatefulWidget {
  const MyTooptip({Key key, this.label, this.child}) : super(key: key);
  final Widget child;
  final String label;

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

class _MyTooptipState extends State<MyTooptip> {
  bool visible = false;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('hello world');
        setState(() {
          visible = !visible;
        });
      },
      child: PortalEntry(
        visible: visible,
        portalAnchor: Alignment.bottomCenter,
        childAnchor: Alignment.topCenter,
        portal: Card(
          color: Colors.grey.shade700,
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(widget.label),
          ),
        ),
        child: widget.child,
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (_, child) => Portal(child: child),
      home: Scaffold(
        appBar: AppBar(title: const Text('tooltip example')),
        body: Center(
          child: MyTooptip(
            label: 'Tooltip',
            child: Card(
              color: Colors.red,
              child: const Padding(
                padding: EdgeInsets.all(8.0),
                child: Text('click me to show a tooltip'),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_portal: ^0.0.1+2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:flutter_portal/flutter_portal.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
11
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
53
Learn more about scoring.

We analyzed this package on Feb 18, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.5
  • Flutter: 1.12.13+hotfix.7

Health suggestions

Format lib/src/portal.dart.

Run flutter format to format lib/src/portal.dart.

Maintenance suggestions

Package is pre-v0.1 release. (-10 points)

While nothing is inherently wrong with versions of 0.0.*, it might mean that the author is still experimenting with the general direction of the API.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test