A new boolean getter `isWaitingForExitingDecision` was added to the route transition record and the `isEntering` getter was renamed to `isWaitingForEnteringDecision`. In the `resolve()` method for the transition delegate, use the `isWaitingForExitingDecision` to check if an exiting route actually needs an explicit decision on how to transition off the screen. If you try to make a decision for an existing route that _isn't_ waiting for a decision, Flutter throws an assertion error.
Original behavior
When the navigator receives a new list of pages, it tries to update its current routes stack to match the list. However, it requires explicit decisions on how to transition the route on and off the screen. Previously, routes that were not in the new list required decisions on how to transition off the screen. However, we later found out this is not always true. If a route is popped, but is still waiting for the popping animation to finish, this route would sit in the navigator routes stack until the animation was done. If a page update occurred during this time, this route exits but doesn't re
New behavior
Not stated in the source.
- Who is affected
- Applications and packages relying on the previous behaviour or signature. Code that uses Navigator, TransitionDelegate, RouteTransitionRecord.
- 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 | import 'package:flutter/widgets.dart'; | |
| 2 | ||
| 3 | class NoAnimationTransitionDelegate extends TransitionDelegate<void> { | |
| 4 | @override | |
| 5 | Iterable<RouteTransitionRecord> resolve({ | |
| 6 | List<RouteTransitionRecord> newPageRouteHistory, | |
| 7 | Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute, | |
| 8 | Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes, | |
| 9 | }) { | |
| 10 | final List<RouteTransitionRecord> results = <RouteTransitionRecord>[]; | |
| 11 | ||
| 12 | for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { | |
| 13 | − | if (pageRoute.isEntering) { |
| 14 | pageRoute.markForAdd(); | |
| 15 | } | |
| 16 | results.add(pageRoute); | |
| 17 | ||
| 18 | } | |
| 19 | for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) { | |
| 20 | − | exitingPageRoute.markForRemove(); |
| 21 | − | final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; |
| 22 | − | if (pagelessRoutes != null) { |
| 23 | − | for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { |
| 24 | − | pagelessRoute.markForRemove(); |
| 25 | } | |
| 26 | } | |
| 27 | results.add(exitingPageRoute); | |
| 28 | ||
| 29 | } | |
| 30 | return results; | |
| 31 | } | |
| 32 | } |
| 1 | import 'package:flutter/widgets.dart'; | |
| 2 | ||
| 3 | class NoAnimationTransitionDelegate extends TransitionDelegate<void> { | |
| 4 | @override | |
| 5 | Iterable<RouteTransitionRecord> resolve({ | |
| 6 | List<RouteTransitionRecord> newPageRouteHistory, | |
| 7 | Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute, | |
| 8 | Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes, | |
| 9 | }) { | |
| 10 | final List<RouteTransitionRecord> results = <RouteTransitionRecord>[]; | |
| 11 | ||
| 12 | for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { | |
| 13 | + | // Renames isEntering to isWaitingForEnteringDecision. |
| 14 | + | if (pageRoute.isWaitingForEnteringDecision) { |
| 15 | pageRoute.markForAdd(); | |
| 16 | } | |
| 17 | results.add(pageRoute); | |
| 18 | ||
| 19 | } | |
| 20 | for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) { | |
| 21 | + | // Checks the isWaitingForExitingDecision before calling the markFor methods. |
| 22 | + | if (exitingPageRoute.isWaitingForExitingDecision) { |
| 23 | + | exitingPageRoute.markForRemove(); |
| 24 | + | final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; |
| 25 | + | if (pagelessRoutes != null) { |
| 26 | + | for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { |
| 27 | + | pagelessRoute.markForRemove(); |
| 28 | + | } |
| 29 | } | |
| 30 | } | |
| 31 | results.add(exitingPageRoute); | |
| 32 | ||
| 33 | } | |
| 34 | return results; | |
| 35 | } | |
| 36 | } |
Raw before / after
import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
if (pageRoute.isEntering) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
results.add(exitingPageRoute);
}
return results;
}
}import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
// Renames isEntering to isWaitingForEnteringDecision.
if (pageRoute.isWaitingForEnteringDecision) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
// Checks the isWaitingForExitingDecision before calling the markFor methods.
if (exitingPageRoute.isWaitingForExitingDecision) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
}
results.add(exitingPageRoute);
}
return results;
}
}Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
If you implement your own transition delegate, you need to check the
exiting routes using the getter isWaitingForExitingDecision before you
call markForPop, markForComplete, or markForRemove on them.
You also need to rename all the references from isEntering to
isWaitingForEnteringDecision.
Code before migration:
import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
if (pageRoute.isEntering) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
results.add(exitingPageRoute);
}
return results;
}
}Code after migration:
import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
// Renames isEntering to isWaitingForEnteringDecision.
if (pageRoute.isWaitingForEnteringDecision) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
// Checks the isWaitingForExitingDecision before calling the markFor methods.
if (exitingPageRoute.isWaitingForExitingDecision) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
}
results.add(exitingPageRoute);
}
return results;
}
}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
- Navigator
- TransitionDelegate
- RouteTransitionRecord
- Issue 45938: Navigator 2.0
- PR 55998
- 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.