{"id":13551799,"url":"https://github.com/fzyzcjy/flutter_portal","last_synced_at":"2025-05-14T22:08:45.971Z","repository":{"id":38836136,"uuid":"231769896","full_name":"fzyzcjy/flutter_portal","owner":"fzyzcjy","description":"Evolved Overlay/OverlayEntry - declarative not imperative, intuitive-context, and easy-alignment","archived":false,"fork":false,"pushed_at":"2024-10-29T23:03:56.000Z","size":1158,"stargazers_count":584,"open_issues_count":0,"forks_count":51,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-05-10T20:01:49.342Z","etag":null,"topics":["dart","flutter","overlay","portal"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/flutter_portal","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fzyzcjy.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-01-04T13:39:17.000Z","updated_at":"2025-05-05T23:50:59.000Z","dependencies_parsed_at":"2024-01-19T06:42:33.771Z","dependency_job_id":"2fbec288-112d-4509-8637-6ebbfbfabf03","html_url":"https://github.com/fzyzcjy/flutter_portal","commit_stats":{"total_commits":330,"total_committers":20,"mean_commits":16.5,"dds":"0.49696969696969695","last_synced_commit":"646cb73a32ffd9f019f6ce1d38b409e72edc2c29"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fzyzcjy%2Fflutter_portal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fzyzcjy%2Fflutter_portal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fzyzcjy%2Fflutter_portal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fzyzcjy%2Fflutter_portal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fzyzcjy","download_url":"https://codeload.github.com/fzyzcjy/flutter_portal/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254235700,"owners_count":22036964,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dart","flutter","overlay","portal"],"created_at":"2024-08-01T12:01:53.872Z","updated_at":"2025-05-14T22:08:40.956Z","avatar_url":"https://github.com/fzyzcjy.png","language":"Dart","funding_links":[],"categories":["Dart"],"sub_categories":[],"readme":"[![Build](https://github.com/fzyzcjy/flutter_portal/actions/workflows/build.yml/badge.svg)](https://github.com/fzyzcjy/flutter_portal/actions/workflows/build.yml)\n[![pub package](https://img.shields.io/pub/v/flutter_portal.svg)](https://pub.dartlang.org/packages/flutter_portal)\n[![codecov](https://codecov.io/gh/fzyzcjy/flutter_portal/branch/master/graph/badge.svg)](https://codecov.io/gh/fzyzcjy/flutter_portal)\n\n# [flutter_portal](https://github.com/fzyzcjy/flutter_portal): Evolved `Overlay`/`OverlayEntry` - declarative not imperative, intuitive-context, and easy-alignment\n\nWant to show floating overlays - tooltips, contextual menus, dialogs, bubbles, etc? This library is an enhancement and replacement to Flutter's built-in [Overlay]/[OverlayEntry].\n\n## 🚀 Advantages\n\nWhy using `flutter_portal` instead of built-in [Overlay]/[OverlayEntry]/[OverlayPortal]?\n\n* **Declarative, not imperative**: Like everything else in the Flutter world, overlays (portals) are declarative now. Simply put your floating UI in the normal widget tree. \u003csub\u003eCompare: The [OverlayEntry] is **not** a widget, and is manipulated imperatively using `.insert()` etc.\u003c/sub\u003e\n* **Alignment, done easily**: Built-in support for aligning an overlay next to a UI component. \u003csub\u003eCompare: A custom contextual menu from scratch in a few lines of code; while [Overlay] makes it nontrivial to align the tooltip/menu next to a widget.\u003c/sub\u003e\n* **Customizable alignment logic**: For example, ensure the portal target never renders outside the screen (`shiftToWithinBound`), align it to portal instead of parent widget (`alignToPortal`), and you can even create your own align algorithm (extend `EnhancedCompositedTransformAnchor`). \u003csub\u003eCompare: [Overlay] does not seem to have this.\u003c/sub\u003e\n* **The intuitive `Context`**: The overlay entry is build with its intuitive parent as its `context`. \u003csub\u003eCompare The [Overlay] approach uses the far-away overlay as its `context`. Update: [OverlayPortal], which is inspired by this package, improved and did this well.\u003c/sub\u003e\n\nAs a consequence, also have the following pros:\n\n* **Easy restorable property**: Since showing an overlay as simple as doing a `setState`, `RestorableProperty` works nicely. \u003csub\u003eCompare: When using the [Overlay] approach, the state of our modals are not restored when our application is killed by the OS.\u003c/sub\u003e\n* **Correct `Theme`/`provider`**: Since the overlay entry has the intuitive `context`, it has access to the same `Theme` and the different `provider`s as the widget that shows the overlay. \u003csub\u003eCompare: The [Overlay] approach will yield confusing Themes and providers. Update: [OverlayPortal], which is inspired by this package, improved and did this well.\u003c/sub\u003e\n\n### 👀 Show me the code\n\n```dart\nPortalTarget(\n  // 1. Declarative: Just provide `portalFollower` as a normal widget\n  // 2. Intuitive BuildContext inside\n  portalFollower: MyAwesomeOverlayWidget(),\n  // 3. Align the \"follower\" relative to the \"child\" anywhere you like\n  anchor: Aligned.center,\n  child: MyChildWidget(),\n)\n```\n\nTo migrate from 0.x to 1.x, see the last section of the readme.\n\n## 🪜 Examples\n\nCheck-out the `examples` folder for examples on how to use flutter_portal:\n\n* [Contextual menu](example/lib/contextual_menu.dart)\n* [Date picker](example/lib/date_picker.dart)\n* [Discovery (Onboarding view)](example/lib/discovery.dart)\n* [Medium clap](example/lib/medium_clap.dart)\n* [Modal](example/lib/modal.dart)\n* [Rounded corners](example/lib/rounded_corners.dart)\n\nPartial screenshots:\n\n| Contextual menu                                                                                                                                                        | Onboarding view                                                                                                                                        |\n| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| \u003cimg width=\"300px\" src=\"https://github.com/fzyzcjy/flutter_portal/raw/master/doc/contextual_menu.png\"\u003e | \u003cimg src=\"https://github.com/fzyzcjy/flutter_portal/raw/master/doc/onboarding_view.gif\" alt=\"Discovery example\" style=\"300px\"\u003e |\n\n## 🧭 Usage\n\n1. Install it. Follow the [standard](https://docs.flutter.dev/development/packages-and-plugins/using-packages) procedure of installing this package. The simplest way may be `flutter pub add flutter_portal`.\n2. Add the [Portal] widget. For example, place it above `MaterialApp`. Only one [Portal] is needed per app.\n3. Use [PortalTarget]s whenever you want to show some overlays.\n\n## 📚 Tutorial: Show a contextual menu\n\nIn this example, we will see how we can use flutter_portal to show a menu\nafter clicking on a `RaisedButton`.\n\n### Add the [Portal] widget\n\nBefore doing anything, you must insert the [Portal] widget in your widget tree. The follower widgets will behave as if they are inserted as children of this widget.\n\nYou can place this [Portal] above `MaterialApp` or near the root of a route:\n\n```dart\nPortal(\n  child: MaterialApp(...)\n)\n```\n\n### The button\n\nFirst, we need to create a `StatefulWidget` that renders our `RaisedButton`:\n\n```dart\nclass MenuExample extends StatefulWidget {\n  @override\n  _MenuExampleState createState() =\u003e _MenuExampleState();\n}\n\nclass _MenuExampleState extends State\u003cMenuExample\u003e {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: RaisedButton(\n          onPressed: () {},\n          child: Text('show menu'),\n        ),\n      ),\n    );\n  }\n}\n```\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/fzyzcjy/flutter_portal/raw/master/doc/usage_a.png\" alt=\"image\" width=\"200px\"\u003e\n\u003c/p\u003e\n\n### The menu - initial iteration\n\nThen, we need to insert our [PortalTarget] in the widget tree.\n\nWe want our contextual menu to render right next to our `RaisedButton`.\nAs such, our [PortalTarget] should be the parent of `RaisedButton` like so:\n\n```dart\nchild: PortalTarget(\n  visible: // TODO\n  anchor: // TODO\n  portalFollower: // TODO\n  child: RaisedButton(...),\n),\n```\n\nWe can pass our menu to [PortalTarget]:\n\n```dart\nPortalTarget(\n  visible: true,\n  anchor: Filled(),\n  portalFollower: Material(\n    elevation: 8,\n    child: IntrinsicWidth(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ListTile(title: Text('option 1')),\n          ListTile(title: Text('option 2')),\n        ],\n      ),\n    ),\n  ),\n  child: RaisedButton(...),\n)\n```\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"200px\" src=\"https://github.com/fzyzcjy/flutter_portal/raw/master/doc/usage_b.png\"\u003e\n\u003c/p\u003e\n\nAt this stage, you may notice two things:\n\n- our menu is full-screen (because `anchor` is `Filled`)\n- our menu is always visible (because `visible` is _true_)\n\n### Change alignment\n\nLet's fix the full-screen issue first and change our code so that our menu renders on the _right_ of our `RaisedButton`.\n\nTo align our menu around our button, we can change the `anchor` parameter:\n\n```dart\nPortalTarget(\n  visible: true,\n  anchor: const Aligned(\n    follower: Alignment.topLeft,\n    target: Alignment.topRight,\n  ),\n  portalFollower: Material(...),\n  child: RaisedButton(...),\n)\n```\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"200px\" src=\"https://github.com/fzyzcjy/flutter_portal/raw/master/doc/usage_c.png\"\u003e\n\u003c/p\u003e\nWhat this code means is, this will align the top-left of our menu with the\ntop-right or the `RaisedButton`. With this, our menu is no-longer full-screen and is now located to the right of our button.\n\n### Show the menu\n\nFinally, we can update our code such that the menu show only when clicking\non the button.\n\nTo do that, we need to declare a new boolean inside our `StatefulWidget`,\nthat says whether the menu is open or not:\n\n```dart\nclass _MenuExampleState extends State\u003cMenuExample\u003e {\n  bool isMenuOpen = false;\n  ...\n}\n```\n\nWe then pass this `isMenuOpen` variable to our [PortalEntry]:\n\n```dart\nPortalTarget(\n  visible: isMenuOpen,\n  ...\n)\n```\n\nThen, inside the `onPressed` callback of our `RaisedButton`, we can\nupdate this `isMenuOpen` variable:\n\n```dart\nRaisedButton(\n  onPressed: () {\n    setState(() {\n      isMenuOpen = true;\n    });\n  },\n  child: Text('show menu'),\n),\n```\n\n### Hide the menu\n\nOne final step is to close the menu when the user clicks randomly outside\nof the menu.\n\nThis can be implemented with a second [PortalEntry] combined with [GestureDetector] like so:\n\n```dart\nPortalTarget(\n  visible: isMenuOpen,\n  portalFollower: GestureDetector(\n    behavior: HitTestBehavior.opaque,\n    onTap: () {\n      setState(() {\n        isMenuOpen = false;\n      });\n    },\n  ),\n  ...\n),\n```\n\n## 🎼 Concepts\n\nThere are a few concepts that are useful to fully understand when using\n`flutter_portal`. That is especially true if you want to support custom use\ncases, which is easily possible with the abstract API provided.\n\nIn the following, each of the abstract concepts you need to understand are\nexplained on a high level. You will find them both in class names (e.g. the\n`Portal` widget or the `PortalTarget` widget as well as in parameter names).\n\n### Portal\n\nA portal (or the portal if you only have one) is the space used for doing all\nof the portal work. On a low level, this means that you have one widget that\nallows its subtree to place targets and followers that are connected.\n\nThe portal also defines the area (rectangle bounds) that are available to any\nfollowers to be rendered onto the screen.\n\nIn detail, you might wrap your whole `MaterialApp` in a single `Portal` widget,\nwhich would mean that you can use the whole area of your app to render followers\nattached to targets that are children of the `Portal` widget.\n\n### Target\n\nA target is any place within a portal that can be followed by a follower. This\nallows you to attach whatever you want to overlay to a specific place in your\nUI, no matter where it moves dynamically.\n\nOn a low level, this means that you wrap the part of your UI that you want to\nfollow in a `PortalTarget` widget and configure it.\n\n#### Example\n\nImagine you want to display tooltips when an avatar is hovered in your app. In\nthat case, the avatar would be the portal **target** and could be used to anchor\nthe tooltip that is overlayed.\n\nAnother example would be a dropdown menu. The widget that shows the current\nselection is the *target* and when tapping on it, the dropdown options would be\noverlayed through the portal as the follower.\n\n### Follower\n\nA follower can only be used in combination with a target. You can use it for\nanything that you want to overlay on top of your UI, attached to a target.\n\nSpecifically, this means that you can pass one `follower` to every\n`PortalTarget`, which will be displayed above your UI within the portal when\nyou specify so.\n\n#### Example\n\nIf you wanted to display an autocomplete text field using `flutter_portal`,\nyou would want to follow the text field to overlay your autocomplete\nsuggestions. The widget for the autocomplete suggestions would be the portal\n**follower** in that case.\n\n### Anchor\n\nAnchors define the layout connection between targets and followers. In general,\nanchors are implemented as an abstract API that provides all the information\nnecessary to support any positioning you want. That means that anchors can be\ndefined based on the attributes of the associated portal, target, and follower.\n\nThere are a few anchors that are implemented by default, e.g. `Aligned` or\n`Filled`.\n\n## ⛵ Migration from 0.x\n\nThere are some breaking changes (mostly introduced by [#44](https://github.com/fzyzcjy/flutter_portal/pull/44)) from 0.x to 1.0, but it can be easily migrated. The following:\n\n```dart\nPortalEntry(\n  portalAnchor: Alignment.topLeft,\n  childAnchor: Alignment.topRight,\n  portal: MyAwesomePortalWidget(),\n  child: MyAwesomeChildWidget(),\n)\n```\n\nBecomes:\n\n```dart\nPortalTarget(\n  anchor: const Aligned(\n    follower: Alignment.topLeft,\n    target: Alignment.topRight,\n  ),\n  portalFollower: MyAwesomePortalWidget(),\n  child: MyAwesomeChildWidget(),\n)\n```\n\nIf you originally use `PortalEntry` without `portalAnchor`/`childAnchor` (i.e. make it fullscreen), then you can write as:\n\n```dart\nPortalTarget(\n  anchor: const Filled(),\n  ...\n)\n```\n\n## ✨ Acknowledgement\n\nOwners\n\n* [@rrousselGit](https://github.com/rrousselGit): The former owner of this package. Create this package in December 2019, and majorly maintain until early 2022. Contributions include: Implementation of the package, including code, documentations, examples, etc. Change algorithms of rendering. Remove PortalEntry's generic. Allow delaying the disappearance of PortalEntry, useful for leave animations. \n* [@fzyzcjy](https://github.com/fzyzcjy): The current owner of this package. See `CHANGELOG.md` for contributions.\n\nContributors\n\n* [@creativecreatorormaybenot](https://github.com/creativecreatorormaybenot): New anchoring logic for advanced use cases, making anchors more flexible, improving code quality, and enhancing non-fragility without additional layout/paint calls.\n* [@Jjagg](https://github.com/Jjagg): Migrate to NNBD.\n* [@CaseyHillers](https://github.com/CaseyHillers): Make example compatible with Dart 3.\n* [@srawlins](https://github.com/srawlins): Update analyzer to make it compatible with Dart 3.\n* [@mono0926](https://github.com/mono0926): Update dependencies and doc.\n* [@tepcii](https://github.com/tepcii) and [@nilsreichardt](https://github.com/nilsreichardt): Fix doc.\n* [@mityax](https://github.com/mityax): Fix export.\n* [@parlough](https://github.com/parlough): Remove outdated lint.\n\n[overlay]: https://api.flutter.dev/flutter/widgets/Overlay-class.html\n[overlayentry]: https://api.flutter.dev/flutter/widgets/OverlayEntry-class.html\n[overlayportal]: https://api.flutter.dev/flutter/widgets/OverlayPortal-class.html\n[portal]: https://pub.dev/documentation/flutter_portal/latest/flutter_portal/Portal-class.html\n[portalentry]: https://pub.dev/documentation/flutter_portal/latest/flutter_portal/PortalEntry-class.html\n[portaltarget]: https://pub.dev/documentation/flutter_portal/latest/flutter_portal/PortalTarget-class.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffzyzcjy%2Fflutter_portal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffzyzcjy%2Fflutter_portal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffzyzcjy%2Fflutter_portal/lists"}