-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
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 onaddPostFrameCallbackinsidebuild. - 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:
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'),
),
);
},
);
}
}