Short answer
Run one navigation at a time. Replace a pop followed by a push with one call, such as pushReplacement or pushAndRemoveUntil; await a dialog before navigating again; and never navigate from dispose(), a popUntil predicate or build(). When the navigation reacts to state (a login result, a stream event), schedule it after the frame with WidgetsBinding.instance.addPostFrameCallback.
Why the Navigator is locked, and the fix
01
A second navigation while the first is still running
A pop inside a popUntil predicate, a showDialog(...).then() that pops again while the dialog’s own pop is running, a pop from dispose() or a pop handler, a pop followed straight away by a push, or a double tap on a back button all start a second operation before the first has finished. Use the single method that does both, or await the first one.
// instead of pop() then push() Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const ResultPage()), ); // back to the first screen and open another Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const HomePage()), (route) => false, ); // a dialog: await it, then navigate once final ok = await showDialog<bool>( context: context, builder: (_) => const ConfirmDialog(), ); if (ok == true && context.mounted) Navigator.of(context).pop();02
Navigator called from build()
“Schedule a callback for the end of this frame.” Flutter, SchedulerBinding.addPostFrameCallback
Redirecting from build() (for example when a FutureBuilder or BlocBuilder sees the user is logged in) navigates in the middle of a frame, and fails when the Navigator itself is rebuilding. Do it after the frame instead, and only once.
if (state.isLoggedIn && !_redirected) { _redirected = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (!context.mounted) return; Navigator.of(context).pushReplacementNamed('/home'); }); }03
A listener navigates while the Navigator is popping
“This method does not request a new frame.” Flutter, SchedulerBinding.addPostFrameCallback
A stream, ChangeNotifier or WillPopScope/PopScope handler that pushes while a pop is still running hits the same lock. Defer the navigation to after the frame. addPostFrameCallback does not request a frame, so when the event can arrive while the app is idle, call ensureVisualUpdate too.
authStream.listen((user) { WidgetsBinding.instance.addPostFrameCallback((_) { navigatorKey.currentState?.pushNamedAndRemoveUntil('/login', (_) => false); }); WidgetsBinding.instance.ensureVisualUpdate(); });
Checked against the vendor’s docs on : Flutter API: SchedulerBinding.addPostFrameCallback.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: Flutter “Navigator operation requested with a context” · Flutter setState() called during build · Flutter “deactivated widget’s ancestor” · Flutter “type 'Null' is not a subtype of type 'String'”