The framework throws an assertion error when it detects there are multiple navigators registered with one hero controller scope.
Original behavior
The hero controller scope hosts a hero controller for its widget subtree. The hero controller can only support one navigator at a time. Previously, there was no assertion to guarantee that.
New behavior
If the code starts throwing assertion errors after this change, it means the code was already broken even before this change. Multiple navigators may be registered under the same hero controller scope, and they can not trigger hero animations when their route changes. This change only surfaced this problem.
- Who is affected
- Applications and packages relying on the previous behaviour or signature. Code that uses Navigator, HeroController, HeroControllerScope.
- 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.
Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
An example application that starts to throw exceptions.
import 'package:material_ui/material_ui.dart';
void main() {
runApp(
MaterialApp(
builder: (BuildContext context, Widget child) {
// Builds two parallel navigators. This throws
// error because both of navigators are under the same
// hero controller scope created by MaterialApp.
return Stack(
children: <Widget>[
Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const Text('first Navigator');
}
);
},
),
Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const Text('Second Navigator');
}
);
},
),
],
);
}
)
);
}You can fix this application by introducing your own hero controller scopes.
import 'package:material_ui/material_ui.dart';
void main() {
runApp(
MaterialApp(
builder: (BuildContext context, Widget child) {
// Builds two parallel navigators.
return Stack(
children: <Widget>[
HeroControllerScope(
controller: MaterialApp.createMaterialHeroController(),
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const Text('first Navigator');
}
);
},
),
),
HeroControllerScope(
controller: MaterialApp.createMaterialHeroController(),
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const Text('second Navigator');
}
);
},
),
),
],
);
}
)
);
}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
- HeroController
- HeroControllerScope
- Issue 45938
- Clean up hero controller scope
- 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.