This optimization improves performance for route transitions, but it may uncover missing calls to `setState` in your app.
Original behavior
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.
New behavior
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.
- Who is affected
- Applications and packages relying on the previous behaviour or signature. Code that uses OverlayEntry, Navigator, Route and related APIs.
- Practical impact
- Behaviour or API differences that can break builds or change runtime behaviour.
- What action should I take?
- Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
Before and after (from the official guide)
| 1 | class FooState extends State<Foo> { | |
| 2 | String buttonLabel = 'Click Me'; | |
| 3 | @override | |
| 4 | Widget build(BuildContext context) { | |
| 5 | return ElevatedButton( | |
| 6 | onPressed: () async { | |
| 7 | − | // Illegal state modification that should be wrapped in setState. |
| 8 | − | buttonLabel = await Navigator.pushNamed(context, '/bar'); |
| 9 | }, | |
| 10 | child: Text(buttonLabel), | |
| 11 | ); | |
| 12 | } | |
| 13 | } |
| 1 | class FooState extends State<Foo> { | |
| 2 | String buttonLabel = 'Click Me'; | |
| 3 | @override | |
| 4 | Widget build(BuildContext context) { | |
| 5 | return ElevatedButton( | |
| 6 | onPressed: () async { | |
| 7 | + | final newLabel = await Navigator.pushNamed(context, '/bar'); |
| 8 | + | setState(() { |
| 9 | + | buttonLabel = newLabel; |
| 10 | + | }); |
| 11 | }, | |
| 12 | child: Text(buttonLabel), | |
| 13 | ); | |
| 14 | } | |
| 15 | } |
Raw before / after
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),
);
}
}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),
);
}
}Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
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),
);
}
}AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).
An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.
- Official migration guide
- setState
- OverlayEntry
- Overlay
- Navigator
- Route
- OverlayRoute
- Issue 45797
- Do not rebuild Routes when a new opaque Route is pushed on top
- Reland "Do not rebuild Routes when a new opaque Route is pushed on top"
- Source commit
- b3045439b7c0
- Source last updated
- Oct 21, 2026
- Last synchronized
- Oct 21, 2026
- Publication status
- published
Publication history
- Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.