Rebuild optimization for OverlayEntries and Routes
Summary
This optimization improves performance for route transitions,
but it may uncover missing calls to setState
in your app.
Context
Prior to this change, an OverlayEntry
would rebuild when
a new opaque entry was added on top of it or removed above it.
These rebuilds were unnecessary because they were not triggered
by a change in state of the affected OverlayEntry
. This
breaking change optimized how we handle the addition and removal of
OverlayEntry
s, and removes unnecessary rebuilds
to improve performance.
Since the Navigator
internally puts each Route
into an
OverlayEntry
this change also applies to Route
transitions:
If an opaque Route
is pushed on top or removed from above another
Route
, the Route
s below the opaque Route
no longer rebuilds unnecessarily.
Description of change
In most cases, this change doesn’t require any changes to your code.
However, if your app was erroneously relying on the implicit
rebuilds you may see issues, which can be resolved by wrapping
any state change in a setState
call.
Furthermore, this change slightly modified the shape of the
widget tree: Prior to this change,
the OverlayEntry
s were wrapped in a Stack
widget.
The explicit Stack
widget was removed from the widget hierarchy.
Migration guide
If you’re seeing issues after upgrading to a Flutter version
that included this change, audit your code for missing calls to
setState
. In the example below, assigning the return value of
Navigator.pushNamed
to buttonLabel
is
implicitly modifying the state and it should be wrapped in an
explicit setState
call.
Code before migration:
class FooState extends State<Foo> {
String buttonLabel = 'Click Me';
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
// Illegal state modification that should be wrapped in setState.
buttonLabel = await Navigator.pushNamed(context, '/bar');
},
child: Text(buttonLabel),
);
}
}
Code after migration:
class FooState extends State<Foo> {
String buttonLabel = 'Click Me';
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
final newLabel = await Navigator.pushNamed(context, '/bar');
setState(() {
buttonLabel = newLabel;
});
},
child: Text(buttonLabel),
);
}
}
Timeline
Landed in version: 1.16.3
In stable release: 1.17
References
API documentation:
Relevant issues:
Relevant PRs: