1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'app.dart';
9/// @docImport 'form.dart';
10/// @docImport 'pages.dart';
11/// @docImport 'pop_scope.dart';
12/// @docImport 'router.dart';
13/// @docImport 'will_pop_scope.dart';
14library;
15
16import 'dart:async';
17import 'dart:collection';
18import 'dart:convert';
19import 'dart:developer' as developer;
20import 'dart:ui' as ui;
21
22import 'package:flutter/foundation.dart';
23import 'package:flutter/rendering.dart';
24import 'package:flutter/scheduler.dart';
25import 'package:flutter/services.dart';
26
27import 'basic.dart';
28import 'binding.dart';
29import 'focus_manager.dart';
30import 'focus_scope.dart';
31import 'focus_traversal.dart';
32import 'framework.dart';
33import 'heroes.dart';
34import 'notification_listener.dart';
35import 'overlay.dart';
36import 'restoration.dart';
37import 'restoration_properties.dart';
38import 'routes.dart';
39import 'ticker_provider.dart';
40
41// Duration for delay before refocusing in android so that the focus won't be interrupted.
42const Duration _kAndroidRefocusingDelayDuration = Duration(milliseconds: 300);
43
44// Examples can assume:
45// typedef MyAppHome = Placeholder;
46// typedef MyHomePage = Placeholder;
47// typedef MyPage = ListTile; // any const widget with a Widget "title" constructor argument would do
48// late NavigatorState navigator;
49// late BuildContext context;
50
51/// Creates a route for the given route settings.
52///
53/// Used by [Navigator.onGenerateRoute].
54///
55/// See also:
56///
57/// * [Navigator], which is where all the [Route]s end up.
58typedef RouteFactory = Route<dynamic>? Function(RouteSettings settings);
59
60/// Creates a series of one or more routes.
61///
62/// Used by [Navigator.onGenerateInitialRoutes].
63typedef RouteListFactory =
64 List<Route<dynamic>> Function(NavigatorState navigator, String initialRoute);
65
66/// Creates a [Route] that is to be added to a [Navigator].
67///
68/// The route can be configured with the provided `arguments`. The provided
69/// `context` is the `BuildContext` of the [Navigator] to which the route is
70/// added.
71///
72/// Used by the restorable methods of the [Navigator] that add anonymous routes
73/// (e.g. [NavigatorState.restorablePush]). For this use case, the
74/// [RestorableRouteBuilder] must be static function annotated with
75/// `@pragma('vm:entry-point')`. The [Navigator] will call it again during
76/// state restoration to re-create the route.
77typedef RestorableRouteBuilder<T> = Route<T> Function(BuildContext context, Object? arguments);
78
79/// Signature for the [Navigator.popUntil] predicate argument.
80typedef RoutePredicate = bool Function(Route<dynamic> route);
81
82/// Signature for a callback that verifies that it's OK to call [Navigator.pop].
83///
84/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback],
85/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
86@Deprecated(
87 'Use PopInvokedCallback instead. '
88 'This feature was deprecated after v3.12.0-1.0.pre.',
89)
90typedef WillPopCallback = Future<bool> Function();
91
92/// Signature for the [Navigator.onPopPage] callback.
93///
94/// This callback must call [Route.didPop] on the specified route and must
95/// properly update the pages list the next time it is passed into
96/// [Navigator.pages] so that it no longer includes the corresponding [Page].
97/// (Otherwise, the page will be interpreted as a new page to show when the
98/// [Navigator.pages] list is next updated.)
99typedef PopPageCallback = bool Function(Route<dynamic> route, dynamic result);
100
101/// Signature for the [Navigator.onDidRemovePage] callback.
102///
103/// This must properly update the pages list the next time it is passed into
104/// [Navigator.pages] so that it no longer includes the input `page`.
105/// (Otherwise, the page will be interpreted as a new page to show when the
106/// [Navigator.pages] list is next updated.)
107typedef DidRemovePageCallback = void Function(Page<Object?> page);
108
109/// Indicates whether the current route should be popped.
110///
111/// Used as the return value for [Route.willPop].
112///
113/// See also:
114///
115/// * [WillPopScope], a widget that hooks into the route's [Route.willPop]
116/// mechanism.
117enum RoutePopDisposition {
118 /// Pop the route.
119 ///
120 /// If [Route.willPop] or [Route.popDisposition] return [pop] then the back
121 /// button will actually pop the current route.
122 pop,
123
124 /// Do not pop the route.
125 ///
126 /// If [Route.willPop] or [Route.popDisposition] return [doNotPop] then the
127 /// back button will be ignored.
128 doNotPop,
129
130 /// Delegate this to the next level of navigation.
131 ///
132 /// If [Route.willPop] or [Route.popDisposition] return [bubble] then the back
133 /// button will be handled by the [SystemNavigator], which will usually close
134 /// the application.
135 bubble,
136}
137
138/// An abstraction for an entry managed by a [Navigator].
139///
140/// This class defines an abstract interface between the navigator and the
141/// "routes" that are pushed on and popped off the navigator. Most routes have
142/// visual affordances, which they place in the navigators [Overlay] using one
143/// or more [OverlayEntry] objects.
144///
145/// See [Navigator] for more explanation of how to use a [Route] with
146/// navigation, including code examples.
147///
148/// See [MaterialPageRoute] for a route that replaces the entire screen with a
149/// platform-adaptive transition.
150///
151/// A route can belong to a page if the [settings] are a subclass of [Page]. A
152/// page-based route, as opposed to a pageless route, is created from
153/// [Page.createRoute] during [Navigator.pages] updates. The page associated
154/// with this route may change during the lifetime of the route. If the
155/// [Navigator] updates the page of this route, it calls [changedInternalState]
156/// to notify the route that the page has been updated.
157///
158/// The type argument `T` is the route's return type, as used by
159/// [currentResult], [popped], and [didPop]. The type `void` may be used if the
160/// route does not return a value.
161abstract class Route<T> extends _RoutePlaceholder {
162 /// Initialize the [Route].
163 ///
164 /// If the [settings] are not provided, an empty [RouteSettings] object is
165 /// used instead.
166 ///
167 /// {@template flutter.widgets.navigator.Route.requestFocus}
168 /// If [requestFocus] is not provided, the value of [Navigator.requestFocus] is
169 /// used instead.
170 /// {@endtemplate}
171 Route({RouteSettings? settings, bool? requestFocus})
172 : _settings = settings ?? const RouteSettings(),
173 _requestFocus = requestFocus {
174 assert(debugMaybeDispatchCreated('widgets', 'Route<T>', this));
175 }
176
177 /// When the route state is updated, request focus if the current route is at the top.
178 ///
179 /// If not provided in the constructor, [Navigator.requestFocus] is used instead.
180 bool get requestFocus => _requestFocus ?? navigator?.widget.requestFocus ?? false;
181 final bool? _requestFocus;
182
183 /// The navigator that the route is in, if any.
184 NavigatorState? get navigator => _navigator;
185 NavigatorState? _navigator;
186
187 /// The settings for this route.
188 ///
189 /// See [RouteSettings] for details.
190 ///
191 /// The settings can change during the route's lifetime. If the settings
192 /// change, the route's overlays will be marked dirty (see
193 /// [changedInternalState]).
194 ///
195 /// If the route is created from a [Page] in the [Navigator.pages] list, then
196 /// this will be a [Page] subclass, and it will be updated each time its
197 /// corresponding [Page] in the [Navigator.pages] has changed. Once the
198 /// [Route] is removed from the history, this value stops updating (and
199 /// remains with its last value).
200 RouteSettings get settings => _settings;
201 RouteSettings _settings;
202
203 bool get _isPageBased => settings is Page<Object?>;
204
205 /// The restoration scope ID to be used for the [RestorationScope] surrounding
206 /// this route.
207 ///
208 /// The restoration scope ID is null if restoration is currently disabled
209 /// for this route.
210 ///
211 /// If the restoration scope ID changes (e.g. because restoration is enabled
212 /// or disabled) during the life of the route, the [ValueListenable] notifies
213 /// its listeners. As an example, the ID changes to null while the route is
214 /// transitioning off screen, which triggers a notification on this field. At
215 /// that point, the route is considered as no longer present for restoration
216 /// purposes and its state will not be restored.
217 ValueListenable<String?> get restorationScopeId => _restorationScopeId;
218 final ValueNotifier<String?> _restorationScopeId = ValueNotifier<String?>(null);
219
220 void _updateSettings(RouteSettings newSettings) {
221 if (_settings != newSettings) {
222 _settings = newSettings;
223 if (_navigator != null) {
224 changedInternalState();
225 }
226 }
227 }
228
229 // ignore: use_setters_to_change_properties, (setters can't be private)
230 void _updateRestorationId(String? restorationId) {
231 _restorationScopeId.value = restorationId;
232 }
233
234 /// The overlay entries of this route.
235 ///
236 /// These are typically populated by [install]. The [Navigator] is in charge
237 /// of adding them to and removing them from the [Overlay].
238 ///
239 /// There must be at least one entry in this list after [install] has been
240 /// invoked.
241 ///
242 /// The [Navigator] will take care of keeping the entries together if the
243 /// route is moved in the history.
244 List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
245
246 /// Called when the route is inserted into the navigator.
247 ///
248 /// Uses this to populate [overlayEntries]. There must be at least one entry in
249 /// this list after [install] has been invoked. The [Navigator] will be in charge
250 /// to add them to the [Overlay] or remove them from it by calling
251 /// [OverlayEntry.remove].
252 @protected
253 @mustCallSuper
254 void install() {}
255
256 /// Called after [install] when the route is pushed onto the navigator.
257 ///
258 /// The returned value resolves when the push transition is complete.
259 ///
260 /// The [didAdd] method will be called instead of [didPush] when the route
261 /// immediately appears on screen without any push transition.
262 ///
263 /// The [didChangeNext] and [didChangePrevious] methods are typically called
264 /// immediately after this method is called.
265 @protected
266 @mustCallSuper
267 TickerFuture didPush() {
268 return TickerFuture.complete()..then<void>((void _) {
269 if (requestFocus) {
270 navigator!.focusNode.enclosingScope?.requestFocus();
271 }
272 });
273 }
274
275 /// Called after [install] when the route is added to the navigator.
276 ///
277 /// This method is called instead of [didPush] when the route immediately
278 /// appears on screen without any push transition.
279 ///
280 /// The [didChangeNext] and [didChangePrevious] methods are typically called
281 /// immediately after this method is called.
282 @protected
283 @mustCallSuper
284 void didAdd() {
285 if (requestFocus) {
286 // This TickerFuture serves two purposes. First, we want to make sure that
287 // animations triggered by other operations will finish before focusing
288 // the navigator. Second, navigator.focusNode might acquire more focused
289 // children in Route.install asynchronously. This TickerFuture will wait
290 // for it to finish first.
291 //
292 // The later case can be found when subclasses manage their own focus scopes.
293 // For example, ModalRoute creates a focus scope in its overlay entries. The
294 // focused child can only be attached to navigator after initState which
295 // will be guarded by the asynchronous gap.
296 TickerFuture.complete().then<void>((void _) {
297 // The route can be disposed before the ticker future completes. This can
298 // happen when the navigator is under a TabView that warps from one tab to
299 // another, non-adjacent tab, with an animation. The TabView reorders its
300 // children before and after the warping completes, and that causes its
301 // children to be built and disposed within the same frame. If one of its
302 // children contains a navigator, the routes in that navigator are also
303 // added and disposed within that frame.
304 //
305 // Since the reference to the navigator will be set to null after it is
306 // disposed, we have to do a null-safe operation in case that happens
307 // within the same frame when it is added.
308 navigator?.focusNode.enclosingScope?.requestFocus();
309 });
310 }
311 }
312
313 /// Called after [install] when the route replaced another in the navigator.
314 ///
315 /// The [didChangeNext] and [didChangePrevious] methods are typically called
316 /// immediately after this method is called.
317 @protected
318 @mustCallSuper
319 void didReplace(Route<dynamic>? oldRoute) {}
320
321 /// Returns whether calling [Navigator.maybePop] when this [Route] is current
322 /// ([isCurrent]) should do anything.
323 ///
324 /// [Navigator.maybePop] is usually used instead of [Navigator.pop] to handle
325 /// the system back button.
326 ///
327 /// By default, if a [Route] is the first route in the history (i.e., if
328 /// [isFirst]), it reports that pops should be bubbled
329 /// ([RoutePopDisposition.bubble]). This behavior prevents the user from
330 /// popping the first route off the history and being stranded at a blank
331 /// screen; instead, the larger scope is popped (e.g. the application quits,
332 /// so that the user returns to the previous application).
333 ///
334 /// In other cases, the default behavior is to accept the pop
335 /// ([RoutePopDisposition.pop]).
336 ///
337 /// The third possible value is [RoutePopDisposition.doNotPop], which causes
338 /// the pop request to be ignored entirely.
339 ///
340 /// See also:
341 ///
342 /// * [Form], which provides a [Form.onWillPop] callback that uses this
343 /// mechanism.
344 /// * [WillPopScope], another widget that provides a way to intercept the
345 /// back button.
346 @Deprecated(
347 'Use popDisposition instead. '
348 'This feature was deprecated after v3.12.0-1.0.pre.',
349 )
350 Future<RoutePopDisposition> willPop() async {
351 return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
352 }
353
354 /// Returns whether calling [Navigator.maybePop] when this [Route] is current
355 /// ([isCurrent]) should do anything.
356 ///
357 /// [Navigator.maybePop] is usually used instead of [Navigator.pop] to handle
358 /// the system back button, when it hasn't been disabled via
359 /// [SystemNavigator.setFrameworkHandlesBack].
360 ///
361 /// By default, if a [Route] is the first route in the history (i.e., if
362 /// [isFirst]), it reports that pops should be bubbled
363 /// ([RoutePopDisposition.bubble]). This behavior prevents the user from
364 /// popping the first route off the history and being stranded at a blank
365 /// screen; instead, the larger scope is popped (e.g. the application quits,
366 /// so that the user returns to the previous application).
367 ///
368 /// In other cases, the default behavior is to accept the pop
369 /// ([RoutePopDisposition.pop]).
370 ///
371 /// The third possible value is [RoutePopDisposition.doNotPop], which causes
372 /// the pop request to be ignored entirely.
373 ///
374 /// See also:
375 ///
376 /// * [Form], which provides a [Form.canPop] boolean that is similar.
377 /// * [PopScope], a widget that provides a way to intercept the back button.
378 /// * [Page.canPop], a way for [Page] to affect this property.
379 RoutePopDisposition get popDisposition {
380 if (_isPageBased) {
381 final Page<Object?> page = settings as Page<Object?>;
382 if (!page.canPop) {
383 return RoutePopDisposition.doNotPop;
384 }
385 }
386 return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
387 }
388
389 /// Called after a route pop was handled.
390 ///
391 /// Even when the pop is canceled, for example by a [PopScope] widget, this
392 /// will still be called. The `didPop` parameter indicates whether or not the
393 /// back navigation actually happened successfully.
394 @Deprecated(
395 'Override onPopInvokedWithResult instead. '
396 'This feature was deprecated after v3.22.0-12.0.pre.',
397 )
398 void onPopInvoked(bool didPop) {}
399
400 /// {@template flutter.widgets.navigator.onPopInvokedWithResult}
401 /// Called after a route pop was handled.
402 ///
403 /// Even when the pop is canceled, for example by a [PopScope] widget, this
404 /// will still be called. The `didPop` parameter indicates whether or not the
405 /// back navigation actually happened successfully.
406 /// {@endtemplate}
407 @mustCallSuper
408 void onPopInvokedWithResult(bool didPop, T? result) {
409 if (_isPageBased) {
410 final Page<T> page = settings as Page<T>;
411 page.onPopInvoked(didPop, result);
412 }
413 }
414
415 /// Whether calling [didPop] would return false.
416 bool get willHandlePopInternally => false;
417
418 /// When this route is popped (see [Navigator.pop]) if the result isn't
419 /// specified or if it's null, this value will be used instead.
420 ///
421 /// This fallback is implemented by [didComplete]. This value is used if the
422 /// argument to that method is null.
423 T? get currentResult => null;
424
425 /// A future that completes when this route is popped off the navigator.
426 ///
427 /// The future completes with the value given to [Navigator.pop], if any, or
428 /// else the value of [currentResult]. See [didComplete] for more discussion
429 /// on this topic.
430 Future<T?> get popped => _popCompleter.future;
431 final Completer<T?> _popCompleter = Completer<T?>();
432
433 final Completer<T?> _disposeCompleter = Completer<T?>();
434
435 /// A request was made to pop this route. If the route can handle it
436 /// internally (e.g. because it has its own stack of internal state) then
437 /// return false, otherwise return true (by returning the value of calling
438 /// `super.didPop`). Returning false will prevent the default behavior of
439 /// [NavigatorState.pop].
440 ///
441 /// When this function returns true, the navigator removes this route from
442 /// the history but does not yet call [dispose]. Instead, it is the route's
443 /// responsibility to call [NavigatorState.finalizeRoute], which will in turn
444 /// call [dispose] on the route. This sequence lets the route perform an
445 /// exit animation (or some other visual effect) after being popped but prior
446 /// to being disposed.
447 ///
448 /// This method should call [didComplete] to resolve the [popped] future (and
449 /// this is all that the default implementation does); routes should not wait
450 /// for their exit animation to complete before doing so.
451 ///
452 /// See [popped], [didComplete], and [currentResult] for a discussion of the
453 /// `result` argument.
454 @mustCallSuper
455 bool didPop(T? result) {
456 didComplete(result);
457 return true;
458 }
459
460 /// The route was popped or is otherwise being removed somewhat gracefully.
461 ///
462 /// This is called by [didPop] and in response to
463 /// [NavigatorState.pushReplacement]. If [didPop] was not called, then the
464 /// [NavigatorState.finalizeRoute] method must be called immediately, and no exit
465 /// animation will run.
466 ///
467 /// The [popped] future is completed by this method. The `result` argument
468 /// specifies the value that this future is completed with, unless it is null,
469 /// in which case [currentResult] is used instead.
470 ///
471 /// This should be called before the pop animation, if any, takes place,
472 /// though in some cases the animation may be driven by the user before the
473 /// route is committed to being popped; this can in particular happen with the
474 /// iOS-style back gesture. See [NavigatorState.didStartUserGesture].
475 @protected
476 @mustCallSuper
477 void didComplete(T? result) {
478 _popCompleter.complete(result ?? currentResult);
479 }
480
481 /// The given route, which was above this one, has been popped off the
482 /// navigator.
483 ///
484 /// This route is now the current route ([isCurrent] is now true), and there
485 /// is no next route.
486 @protected
487 @mustCallSuper
488 void didPopNext(Route<dynamic> nextRoute) {}
489
490 /// This route's next route has changed to the given new route.
491 ///
492 /// This is called on a route whenever the next route changes for any reason,
493 /// so long as it is in the history, including when a route is first added to
494 /// a [Navigator] (e.g. by [Navigator.push]), except for cases when
495 /// [didPopNext] would be called.
496 ///
497 /// The `nextRoute` argument will be null if there's no new next route (i.e.
498 /// if [isCurrent] is true).
499 @protected
500 @mustCallSuper
501 void didChangeNext(Route<dynamic>? nextRoute) {}
502
503 /// This route's previous route has changed to the given new route.
504 ///
505 /// This is called on a route whenever the previous route changes for any
506 /// reason, so long as it is in the history, except for immediately after the
507 /// route itself has been pushed (in which case [didPush] or [didReplace] will
508 /// be called instead).
509 ///
510 /// The `previousRoute` argument will be null if there's no previous route
511 /// (i.e. if [isFirst] is true).
512 @protected
513 @mustCallSuper
514 void didChangePrevious(Route<dynamic>? previousRoute) {}
515
516 /// Called whenever the internal state of the route has changed.
517 ///
518 /// This should be called whenever [willHandlePopInternally], [didPop],
519 /// [ModalRoute.offstage], or other internal state of the route changes value.
520 /// It is used by [ModalRoute], for example, to report the new information via
521 /// its inherited widget to any children of the route.
522 ///
523 /// See also:
524 ///
525 /// * [changedExternalState], which is called when the [Navigator] has
526 /// updated in some manner that might affect the routes.
527 @protected
528 @mustCallSuper
529 void changedInternalState() {}
530
531 /// Called whenever the [Navigator] has updated in some manner that might
532 /// affect routes, to indicate that the route may wish to rebuild as well.
533 ///
534 /// This is called by the [Navigator] whenever the
535 /// [NavigatorState]'s [State.widget] changes (as in [State.didUpdateWidget]),
536 /// for example because the [MaterialApp] has been rebuilt. This
537 /// ensures that routes that directly refer to the state of the
538 /// widget that built the [MaterialApp] will be notified when that
539 /// widget rebuilds, since it would otherwise be difficult to notify
540 /// the routes that state they depend on may have changed.
541 ///
542 /// It is also called whenever the [Navigator]'s dependencies change
543 /// (as in [State.didChangeDependencies]). This allows routes to use the
544 /// [Navigator]'s context ([NavigatorState.context]), for example in
545 /// [ModalRoute.barrierColor], and update accordingly.
546 ///
547 /// The [ModalRoute] subclass overrides this to force the barrier
548 /// overlay to rebuild.
549 ///
550 /// See also:
551 ///
552 /// * [changedInternalState], the equivalent but for changes to the internal
553 /// state of the route.
554 @protected
555 @mustCallSuper
556 void changedExternalState() {}
557
558 /// Discards any resources used by the object.
559 ///
560 /// This method should not remove its [overlayEntries] from the [Overlay]. The
561 /// object's owner is in charge of doing that.
562 ///
563 /// After this is called, the object is not in a usable state and should be
564 /// discarded.
565 ///
566 /// This method should only be called by the object's owner; typically the
567 /// [Navigator] owns a route and so will call this method when the route is
568 /// removed, after which the route is no longer referenced by the navigator.
569 @mustCallSuper
570 @protected
571 void dispose() {
572 _navigator = null;
573 _restorationScopeId.dispose();
574 _disposeCompleter.complete();
575 assert(debugMaybeDispatchDisposed(this));
576 }
577
578 /// Whether this route is the top-most route on the navigator.
579 ///
580 /// If this is true, then [isActive] is also true.
581 bool get isCurrent {
582 if (_navigator == null) {
583 return false;
584 }
585 final _RouteEntry? currentRouteEntry = _navigator!._lastRouteEntryWhereOrNull(
586 _RouteEntry.isPresentPredicate,
587 );
588 if (currentRouteEntry == null) {
589 return false;
590 }
591 return currentRouteEntry.route == this;
592 }
593
594 /// Whether this route is the bottom-most active route on the navigator.
595 ///
596 /// If [isFirst] and [isCurrent] are both true then this is the only route on
597 /// the navigator (and [isActive] will also be true).
598 bool get isFirst {
599 if (_navigator == null) {
600 return false;
601 }
602 final _RouteEntry? currentRouteEntry = _navigator!._firstRouteEntryWhereOrNull(
603 _RouteEntry.isPresentPredicate,
604 );
605 if (currentRouteEntry == null) {
606 return false;
607 }
608 return currentRouteEntry.route == this;
609 }
610
611 /// Whether there is at least one active route underneath this route.
612 @protected
613 bool get hasActiveRouteBelow {
614 if (_navigator == null) {
615 return false;
616 }
617 for (final _RouteEntry entry in _navigator!._history) {
618 if (entry.route == this) {
619 return false;
620 }
621 if (_RouteEntry.isPresentPredicate(entry)) {
622 return true;
623 }
624 }
625 return false;
626 }
627
628 /// Whether this route is on the navigator.
629 ///
630 /// If the route is not only active, but also the current route (the top-most
631 /// route), then [isCurrent] will also be true. If it is the first route (the
632 /// bottom-most route), then [isFirst] will also be true.
633 ///
634 /// If a higher route is entirely opaque, then the route will be active but not
635 /// rendered. It is even possible for the route to be active but for the stateful
636 /// widgets within the route to not be instantiated. See [ModalRoute.maintainState].
637 bool get isActive {
638 return _navigator?._firstRouteEntryWhereOrNull(_RouteEntry.isRoutePredicate(this))?.isPresent ??
639 false;
640 }
641}
642
643/// Data that might be useful in constructing a [Route].
644@immutable
645class RouteSettings {
646 /// Creates data used to construct routes.
647 const RouteSettings({this.name, this.arguments});
648
649 /// The name of the route (e.g., "/settings").
650 ///
651 /// If null, the route is anonymous.
652 final String? name;
653
654 /// The arguments passed to this route.
655 ///
656 /// May be used when building the route, e.g. in [Navigator.onGenerateRoute].
657 final Object? arguments;
658
659 @override
660 String toString() =>
661 '${objectRuntimeType(this, 'RouteSettings')}(${name == null ? 'none' : '"$name"'}, $arguments)';
662}
663
664/// Describes the configuration of a [Route].
665///
666/// The type argument `T` is the corresponding [Route]'s return type, as
667/// used by [Route.currentResult], [Route.popped], and [Route.didPop].
668///
669/// The [canPop] and [onPopInvoked] are used for intercepting pops.
670///
671/// {@tool dartpad}
672/// This sample demonstrates how to use this [canPop] and [onPopInvoked] to
673/// intercept pops.
674///
675/// ** See code in examples/api/lib/widgets/page/page_can_pop.0.dart **
676/// {@end-tool}
677///
678/// See also:
679///
680/// * [Navigator.pages], which accepts a list of [Page]s and updates its routes
681/// history.
682abstract class Page<T> extends RouteSettings {
683 /// Creates a page and initializes [key] for subclasses.
684 const Page({
685 this.key,
686 super.name,
687 super.arguments,
688 this.restorationId,
689 this.canPop = true,
690 this.onPopInvoked = _defaultPopInvokedHandler,
691 });
692
693 static void _defaultPopInvokedHandler(bool didPop, Object? result) {}
694
695 /// The key associated with this page.
696 ///
697 /// This key will be used for comparing pages in [canUpdate].
698 final LocalKey? key;
699
700 /// Restoration ID to save and restore the state of the [Route] configured by
701 /// this page.
702 ///
703 /// If no restoration ID is provided, the [Route] will not restore its state.
704 ///
705 /// See also:
706 ///
707 /// * [RestorationManager], which explains how state restoration works in
708 /// Flutter.
709 final String? restorationId;
710
711 /// Called after a pop on the associated route was handled.
712 ///
713 /// It's not possible to prevent the pop from happening at the time that this
714 /// method is called; the pop has already happened. Use [canPop] to
715 /// disable pops in advance.
716 ///
717 /// This will still be called even when the pop is canceled. A pop is canceled
718 /// when the associated [Route.popDisposition] returns false, or when
719 /// [canPop] is set to false. The `didPop` parameter indicates whether or not
720 /// the back navigation actually happened successfully.
721 final PopInvokedWithResultCallback<T> onPopInvoked;
722
723 /// When false, blocks the associated route from being popped.
724 ///
725 /// If this is set to false for first page in the Navigator. It prevents
726 /// Flutter app from exiting.
727 ///
728 /// If there are any [PopScope] widgets in a route's widget subtree,
729 /// each of their `canPop` must be `true`, in addition to this canPop, in
730 /// order for the route to be able to pop.
731 final bool canPop;
732
733 /// Whether this page can be updated with the [other] page.
734 ///
735 /// Two pages are consider updatable if they have same the [runtimeType] and
736 /// [key].
737 bool canUpdate(Page<dynamic> other) {
738 return other.runtimeType == runtimeType && other.key == key;
739 }
740
741 /// Creates the [Route] that corresponds to this page.
742 ///
743 /// The created [Route] must have its [Route.settings] property set to this [Page].
744 @factory
745 Route<T> createRoute(BuildContext context);
746
747 @override
748 String toString() => '${objectRuntimeType(this, 'Page')}("$name", $key, $arguments)';
749}
750
751/// An interface for observing the behavior of a [Navigator].
752class NavigatorObserver {
753 /// The navigator that the observer is observing, if any.
754 NavigatorState? get navigator => _navigators[this];
755
756 // Expando mapping instances of NavigatorObserver to their associated
757 // NavigatorState (or `null`, if there is no associated NavigatorState). The
758 // reason we don't use a private instance field of type
759 // `NavigatorState?` is because as part of implementing
760 // https://github.com/dart-lang/language/issues/2020, it will soon become a
761 // runtime error to invoke a private member that is mocked in another
762 // library. By using an expando rather than an instance field, we ensure
763 // that a mocked NavigatorObserver can still properly keep track of its
764 // associated NavigatorState.
765 static final Expando<NavigatorState> _navigators = Expando<NavigatorState>();
766
767 /// The [Navigator] pushed `route`.
768 ///
769 /// The route immediately below that one, and thus the previously active
770 /// route, is `previousRoute`.
771 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {}
772
773 /// The [Navigator] popped `route`.
774 ///
775 /// The route immediately below that one, and thus the newly active
776 /// route, is `previousRoute`.
777 void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {}
778
779 /// The [Navigator] removed `route`.
780 ///
781 /// If only one route is being removed, then the route immediately below
782 /// that one, if any, is `previousRoute`.
783 ///
784 /// If multiple routes are being removed, then the route below the
785 /// bottommost route being removed, if any, is `previousRoute`, and this
786 /// method will be called once for each removed route, from the topmost route
787 /// to the bottommost route.
788 void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {}
789
790 /// The [Navigator] replaced `oldRoute` with `newRoute`.
791 void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {}
792
793 /// The top most route has changed.
794 ///
795 /// The `topRoute` is the new top most route. This can be a new route pushed
796 /// on top of the screen, or an existing route that becomes the new top-most
797 /// route because the previous top-most route has been popped.
798 ///
799 /// The `previousTopRoute` was the top most route before the change. This can
800 /// be a route that was popped off the screen, or a route that will be covered
801 /// by the `topRoute`. This can also be null if this is the first build.
802 void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) {}
803
804 /// The [Navigator]'s routes are being moved by a user gesture.
805 ///
806 /// For example, this is called when an iOS back gesture starts, and is used
807 /// to disable hero animations during such interactions.
808 void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) {}
809
810 /// User gesture is no longer controlling the [Navigator].
811 ///
812 /// Paired with an earlier call to [didStartUserGesture].
813 void didStopUserGesture() {}
814}
815
816/// An inherited widget to host a hero controller.
817///
818/// The hosted hero controller will be picked up by the navigator in the
819/// [child] subtree. Once a navigator picks up this controller, the navigator
820/// will bar any navigator below its subtree from receiving this controller.
821///
822/// The hero controller inside the [HeroControllerScope] can only subscribe to
823/// one navigator at a time. An assertion will be thrown if the hero controller
824/// subscribes to more than one navigators. This can happen when there are
825/// multiple navigators under the same [HeroControllerScope] in parallel.
826class HeroControllerScope extends InheritedWidget {
827 /// Creates a widget to host the input [controller].
828 const HeroControllerScope({
829 super.key,
830 required HeroController this.controller,
831 required super.child,
832 });
833
834 /// Creates a widget to prevent the subtree from receiving the hero controller
835 /// above.
836 const HeroControllerScope.none({super.key, required super.child}) : controller = null;
837
838 /// The hero controller that is hosted inside this widget.
839 final HeroController? controller;
840
841 /// Retrieves the [HeroController] from the closest [HeroControllerScope]
842 /// ancestor, or null if none exists.
843 ///
844 /// Calling this method will create a dependency on the closest
845 /// [HeroControllerScope] in the [context], if there is one.
846 ///
847 /// See also:
848 ///
849 /// * [HeroControllerScope.of], which is similar to this method, but asserts
850 /// if no [HeroControllerScope] ancestor is found.
851 static HeroController? maybeOf(BuildContext context) {
852 final HeroControllerScope? host = context
853 .dependOnInheritedWidgetOfExactType<HeroControllerScope>();
854 return host?.controller;
855 }
856
857 /// Retrieves the [HeroController] from the closest [HeroControllerScope]
858 /// ancestor.
859 ///
860 /// If no ancestor is found, this method will assert in debug mode, and throw
861 /// an exception in release mode.
862 ///
863 /// Calling this method will create a dependency on the closest
864 /// [HeroControllerScope] in the [context].
865 ///
866 /// See also:
867 ///
868 /// * [HeroControllerScope.maybeOf], which is similar to this method, but
869 /// returns null if no [HeroControllerScope] ancestor is found.
870 static HeroController of(BuildContext context) {
871 final HeroController? controller = maybeOf(context);
872 assert(() {
873 if (controller == null) {
874 throw FlutterError(
875 'HeroControllerScope.of() was called with a context that does not contain a '
876 'HeroControllerScope widget.\n'
877 'No HeroControllerScope widget ancestor could be found starting from the '
878 'context that was passed to HeroControllerScope.of(). This can happen '
879 'because you are using a widget that looks for a HeroControllerScope '
880 'ancestor, but no such ancestor exists.\n'
881 'The context used was:\n'
882 ' $context',
883 );
884 }
885 return true;
886 }());
887 return controller!;
888 }
889
890 @override
891 bool updateShouldNotify(HeroControllerScope oldWidget) {
892 return oldWidget.controller != controller;
893 }
894}
895
896/// A [Route] wrapper interface that can be staged for [TransitionDelegate] to
897/// decide how its underlying [Route] should transition on or off screen.
898abstract class RouteTransitionRecord {
899 /// Retrieves the wrapped [Route].
900 Route<dynamic> get route;
901
902 /// Whether this route is waiting for the decision on how to enter the screen.
903 ///
904 /// If this property is true, this route requires an explicit decision on how
905 /// to transition into the screen. Such a decision should be made in the
906 /// [TransitionDelegate.resolve].
907 bool get isWaitingForEnteringDecision;
908
909 /// Whether this route is waiting for the decision on how to exit the screen.
910 ///
911 /// If this property is true, this route requires an explicit decision on how
912 /// to transition off the screen. Such a decision should be made in the
913 /// [TransitionDelegate.resolve].
914 bool get isWaitingForExitingDecision;
915
916 /// Marks the [route] to be pushed with transition.
917 ///
918 /// During [TransitionDelegate.resolve], this can be called on an entering
919 /// route (where [RouteTransitionRecord.isWaitingForEnteringDecision] is true) in indicate that the
920 /// route should be pushed onto the [Navigator] with an animated transition.
921 void markForPush();
922
923 /// Marks the [route] to be added without transition.
924 ///
925 /// During [TransitionDelegate.resolve], this can be called on an entering
926 /// route (where [RouteTransitionRecord.isWaitingForEnteringDecision] is true) in indicate that the
927 /// route should be added onto the [Navigator] without an animated transition.
928 void markForAdd();
929
930 /// Marks the [route] to be popped with transition.
931 ///
932 /// During [TransitionDelegate.resolve], this can be called on an exiting
933 /// route to indicate that the route should be popped off the [Navigator] with
934 /// an animated transition.
935 void markForPop([dynamic result]);
936
937 /// Marks the [route] to be completed without transition.
938 ///
939 /// During [TransitionDelegate.resolve], this can be called on an exiting
940 /// route to indicate that the route should be completed with the provided
941 /// result and removed from the [Navigator] without an animated transition.
942 void markForComplete([dynamic result]);
943
944 /// Marks the [route] to be removed without transition.
945 ///
946 /// During [TransitionDelegate.resolve], this can be called on an exiting
947 /// route to indicate that the route should be removed from the [Navigator]
948 /// without completing and without an animated transition.
949 @Deprecated(
950 'Call markForComplete instead. '
951 'This will let route associated future to complete when route is removed. '
952 'This feature was deprecated after v3.27.0-1.0.pre.',
953 )
954 void markForRemove() => markForComplete();
955}
956
957/// The delegate that decides how pages added and removed from [Navigator.pages]
958/// transition in or out of the screen.
959///
960/// This abstract class implements the API to be called by [Navigator] when it
961/// requires explicit decisions on how the routes transition on or off the screen.
962///
963/// To make route transition decisions, subclass must implement [resolve].
964///
965/// {@tool snippet}
966/// The following example demonstrates how to implement a subclass that always
967/// removes or adds routes without animated transitions and puts the removed
968/// routes at the top of the list.
969///
970/// ```dart
971/// class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
972/// @override
973/// Iterable<RouteTransitionRecord> resolve({
974/// required List<RouteTransitionRecord> newPageRouteHistory,
975/// required Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute,
976/// required Map<RouteTransitionRecord?, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
977/// }) {
978/// final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
979///
980/// for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
981/// if (pageRoute.isWaitingForEnteringDecision) {
982/// pageRoute.markForAdd();
983/// }
984/// results.add(pageRoute);
985///
986/// }
987/// for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
988/// if (exitingPageRoute.isWaitingForExitingDecision) {
989/// exitingPageRoute.markForComplete();
990/// final List<RouteTransitionRecord>? pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
991/// if (pagelessRoutes != null) {
992/// for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
993/// pagelessRoute.markForComplete();
994/// }
995/// }
996/// }
997/// results.add(exitingPageRoute);
998///
999/// }
1000/// return results;
1001/// }
1002/// }
1003///
1004/// ```
1005/// {@end-tool}
1006///
1007/// See also:
1008///
1009/// * [Navigator.transitionDelegate], which uses this class to make route
1010/// transition decisions.
1011/// * [DefaultTransitionDelegate], which implements the default way to decide
1012/// how routes transition in or out of the screen.
1013abstract class TransitionDelegate<T> {
1014 /// Creates a delegate and enables subclass to create a constant class.
1015 const TransitionDelegate();
1016
1017 Iterable<RouteTransitionRecord> _transition({
1018 required List<RouteTransitionRecord> newPageRouteHistory,
1019 required Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute,
1020 required Map<RouteTransitionRecord?, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
1021 }) {
1022 final Iterable<RouteTransitionRecord> results = resolve(
1023 newPageRouteHistory: newPageRouteHistory,
1024 locationToExitingPageRoute: locationToExitingPageRoute,
1025 pageRouteToPagelessRoutes: pageRouteToPagelessRoutes,
1026 );
1027 // Verifies the integrity after the decisions have been made.
1028 //
1029 // Here are the rules:
1030 // - All the entering routes in newPageRouteHistory must either be pushed or
1031 // added.
1032 // - All the exiting routes in locationToExitingPageRoute must either be
1033 // popped, completed or removed.
1034 // - All the pageless routes that belong to exiting routes must either be
1035 // popped, completed or removed.
1036 // - All the entering routes in the result must preserve the same order as
1037 // the entering routes in newPageRouteHistory, and the result must contain
1038 // all exiting routes.
1039 // ex:
1040 //
1041 // newPageRouteHistory = [A, B, C]
1042 //
1043 // locationToExitingPageRoute = {A -> D, C -> E}
1044 //
1045 // results = [A, B ,C ,D ,E] is valid
1046 // results = [D, A, B ,C ,E] is also valid because exiting route can be
1047 // inserted in any place
1048 //
1049 // results = [B, A, C ,D ,E] is invalid because B must be after A.
1050 // results = [A, B, C ,E] is invalid because results must include D.
1051 assert(() {
1052 final List<RouteTransitionRecord> resultsToVerify = results.toList(growable: false);
1053 final Set<RouteTransitionRecord> exitingPageRoutes = locationToExitingPageRoute.values
1054 .toSet();
1055 // Firstly, verifies all exiting routes have been marked.
1056 for (final RouteTransitionRecord exitingPageRoute in exitingPageRoutes) {
1057 assert(!exitingPageRoute.isWaitingForExitingDecision);
1058 if (pageRouteToPagelessRoutes.containsKey(exitingPageRoute)) {
1059 for (final RouteTransitionRecord pagelessRoute
1060 in pageRouteToPagelessRoutes[exitingPageRoute]!) {
1061 assert(!pagelessRoute.isWaitingForExitingDecision);
1062 }
1063 }
1064 }
1065 // Secondly, verifies the order of results matches the newPageRouteHistory
1066 // and contains all the exiting routes.
1067 int indexOfNextRouteInNewHistory = 0;
1068
1069 for (final _RouteEntry routeEntry in resultsToVerify.cast<_RouteEntry>()) {
1070 assert(!routeEntry.isWaitingForEnteringDecision && !routeEntry.isWaitingForExitingDecision);
1071 if (indexOfNextRouteInNewHistory >= newPageRouteHistory.length ||
1072 routeEntry != newPageRouteHistory[indexOfNextRouteInNewHistory]) {
1073 assert(exitingPageRoutes.contains(routeEntry));
1074 exitingPageRoutes.remove(routeEntry);
1075 } else {
1076 indexOfNextRouteInNewHistory += 1;
1077 }
1078 }
1079
1080 assert(
1081 indexOfNextRouteInNewHistory == newPageRouteHistory.length && exitingPageRoutes.isEmpty,
1082 'The merged result from the $runtimeType.resolve does not include all '
1083 'required routes. Do you remember to merge all exiting routes?',
1084 );
1085 return true;
1086 }());
1087
1088 return results;
1089 }
1090
1091 /// A method that will be called by the [Navigator] to decide how routes
1092 /// transition in or out of the screen when [Navigator.pages] is updated.
1093 ///
1094 /// The `newPageRouteHistory` list contains all page-based routes in the order
1095 /// that will be on the [Navigator]'s history stack after this update
1096 /// completes. If a route in `newPageRouteHistory` has its
1097 /// [RouteTransitionRecord.isWaitingForEnteringDecision] set to true, this
1098 /// route requires explicit decision on how it should transition onto the
1099 /// Navigator. To make a decision, call [RouteTransitionRecord.markForPush] or
1100 /// [RouteTransitionRecord.markForAdd].
1101 ///
1102 /// The `locationToExitingPageRoute` contains the pages-based routes that
1103 /// are removed from the routes history after page update. This map records
1104 /// page-based routes to be removed with the location of the route in the
1105 /// original route history before the update. The keys are the locations
1106 /// represented by the page-based routes that are directly below the removed
1107 /// routes, and the value are the page-based routes to be removed. The
1108 /// location is null if the route to be removed is the bottom most route. If
1109 /// a route in `locationToExitingPageRoute` has its
1110 /// [RouteTransitionRecord.isWaitingForExitingDecision] set to true, this
1111 /// route requires explicit decision on how it should transition off the
1112 /// Navigator. To make a decision for a removed route, call
1113 /// [RouteTransitionRecord.markForPop],
1114 /// [RouteTransitionRecord.markForComplete]. It is possible that decisions are
1115 /// not required for routes in the `locationToExitingPageRoute`. This can
1116 /// happen if the routes have already been popped in earlier page updates and
1117 /// are still waiting for popping animations to finish. In such case, those
1118 /// routes are still included in the `locationToExitingPageRoute` with their
1119 /// [RouteTransitionRecord.isWaitingForExitingDecision] set to false and no
1120 /// decisions are required.
1121 ///
1122 /// The `pageRouteToPagelessRoutes` records the page-based routes and their
1123 /// associated pageless routes. If a page-based route is waiting for exiting
1124 /// decision, its associated pageless routes also require explicit decisions
1125 /// on how to transition off the screen.
1126 ///
1127 /// Once all the decisions have been made, this method must merge the removed
1128 /// routes (whether or not they require decisions) and the
1129 /// `newPageRouteHistory` and return the merged result. The order in the
1130 /// result will be the order the [Navigator] uses for updating the route
1131 /// history. The return list must preserve the same order of routes in
1132 /// `newPageRouteHistory`. The removed routes, however, can be inserted into
1133 /// the return list freely as long as all of them are included.
1134 ///
1135 /// For example, consider the following case.
1136 ///
1137 /// `newPageRouteHistory = [A, B, C]`
1138 ///
1139 /// `locationToExitingPageRoute = {A -> D, C -> E}`
1140 ///
1141 /// The following outputs are valid.
1142 ///
1143 /// `result = [A, B ,C ,D ,E]` is valid.
1144 /// `result = [D, A, B ,C ,E]` is also valid because exiting route can be
1145 /// inserted in any place.
1146 ///
1147 /// The following outputs are invalid.
1148 ///
1149 /// `result = [B, A, C ,D ,E]` is invalid because B must be after A.
1150 /// `result = [A, B, C ,E]` is invalid because results must include D.
1151 ///
1152 /// See also:
1153 ///
1154 /// * [RouteTransitionRecord.markForPush], which makes route enter the screen
1155 /// with an animated transition.
1156 /// * [RouteTransitionRecord.markForAdd], which makes route enter the screen
1157 /// without an animated transition.
1158 /// * [RouteTransitionRecord.markForPop], which makes route exit the screen
1159 /// with an animated transition.
1160 /// * [RouteTransitionRecord.markForComplete], which completes the route and
1161 /// makes it exit the screen without an animated transition.
1162 /// * [DefaultTransitionDelegate.resolve], which implements the default way
1163 /// to decide how routes transition in or out of the screen.
1164 Iterable<RouteTransitionRecord> resolve({
1165 required List<RouteTransitionRecord> newPageRouteHistory,
1166 required Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute,
1167 required Map<RouteTransitionRecord?, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
1168 });
1169}
1170
1171/// The default implementation of [TransitionDelegate] that the [Navigator] will
1172/// use if its [Navigator.transitionDelegate] is not specified.
1173///
1174/// This transition delegate follows two rules. Firstly, all the entering routes
1175/// are placed on top of the exiting routes if they are at the same location.
1176/// Secondly, the top most route will always transition with an animated transition.
1177/// All the other routes below will either be completed with
1178/// [Route.currentResult] or added without an animated transition.
1179class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
1180 /// Creates a default transition delegate.
1181 const DefaultTransitionDelegate() : super();
1182
1183 @override
1184 Iterable<RouteTransitionRecord> resolve({
1185 required List<RouteTransitionRecord> newPageRouteHistory,
1186 required Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute,
1187 required Map<RouteTransitionRecord?, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
1188 }) {
1189 final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
1190 // This method will handle the exiting route and its corresponding pageless
1191 // route at this location. It will also recursively check if there is any
1192 // other exiting routes above it and handle them accordingly.
1193 void handleExitingRoute(RouteTransitionRecord? location, bool isLast) {
1194 final RouteTransitionRecord? exitingPageRoute = locationToExitingPageRoute[location];
1195 if (exitingPageRoute == null) {
1196 return;
1197 }
1198 if (exitingPageRoute.isWaitingForExitingDecision) {
1199 final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute);
1200 final bool isLastExitingPageRoute =
1201 isLast && !locationToExitingPageRoute.containsKey(exitingPageRoute);
1202 if (isLastExitingPageRoute && !hasPagelessRoute) {
1203 exitingPageRoute.markForPop(exitingPageRoute.route.currentResult);
1204 } else {
1205 exitingPageRoute.markForComplete(exitingPageRoute.route.currentResult);
1206 }
1207 if (hasPagelessRoute) {
1208 final List<RouteTransitionRecord> pagelessRoutes =
1209 pageRouteToPagelessRoutes[exitingPageRoute]!;
1210 for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
1211 // It is possible that a pageless route that belongs to an exiting
1212 // page-based route does not require exiting decision. This can
1213 // happen if the page list is updated right after a Navigator.pop.
1214 if (pagelessRoute.isWaitingForExitingDecision) {
1215 if (isLastExitingPageRoute && pagelessRoute == pagelessRoutes.last) {
1216 pagelessRoute.markForPop(pagelessRoute.route.currentResult);
1217 } else {
1218 pagelessRoute.markForComplete(pagelessRoute.route.currentResult);
1219 }
1220 }
1221 }
1222 }
1223 }
1224 results.add(exitingPageRoute);
1225
1226 // It is possible there is another exiting route above this exitingPageRoute.
1227 handleExitingRoute(exitingPageRoute, isLast);
1228 }
1229
1230 // Handles exiting route in the beginning of list.
1231 handleExitingRoute(null, newPageRouteHistory.isEmpty);
1232
1233 for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
1234 final bool isLastIteration = newPageRouteHistory.last == pageRoute;
1235 if (pageRoute.isWaitingForEnteringDecision) {
1236 if (!locationToExitingPageRoute.containsKey(pageRoute) && isLastIteration) {
1237 pageRoute.markForPush();
1238 } else {
1239 pageRoute.markForAdd();
1240 }
1241 }
1242 results.add(pageRoute);
1243 handleExitingRoute(pageRoute, isLastIteration);
1244 }
1245 return results;
1246 }
1247}
1248
1249/// The default value of [Navigator.routeTraversalEdgeBehavior].
1250///
1251/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
1252const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = TraversalEdgeBehavior.parentScope;
1253
1254/// The default value of [Navigator.routeDirectionalTraversalEdgeBehavior].
1255///
1256/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
1257const TraversalEdgeBehavior kDefaultRouteDirectionalTraversalEdgeBehavior =
1258 TraversalEdgeBehavior.stop;
1259
1260/// A widget that manages a set of child widgets with a stack discipline.
1261///
1262/// Many apps have a navigator near the top of their widget hierarchy in order
1263/// to display their logical history using an [Overlay] with the most recently
1264/// visited pages visually on top of the older pages. Using this pattern lets
1265/// the navigator visually transition from one page to another by moving the widgets
1266/// around in the overlay. Similarly, the navigator can be used to show a dialog
1267/// by positioning the dialog widget above the current page.
1268///
1269/// ## Using the Pages API
1270///
1271/// The [Navigator] will convert its [Navigator.pages] into a stack of [Route]s
1272/// if it is provided. A change in [Navigator.pages] will trigger an update to
1273/// the stack of [Route]s. The [Navigator] will update its routes to match the
1274/// new configuration of its [Navigator.pages]. To use this API, one can create
1275/// a [Page] subclass and defines a list of [Page]s for [Navigator.pages]. A
1276/// [Navigator.onPopPage] callback is also required to properly clean up the
1277/// input pages in case of a pop.
1278///
1279/// By Default, the [Navigator] will use [DefaultTransitionDelegate] to decide
1280/// how routes transition in or out of the screen. To customize it, define a
1281/// [TransitionDelegate] subclass and provide it to the
1282/// [Navigator.transitionDelegate].
1283///
1284/// For more information on using the pages API, see the [Router] widget.
1285///
1286/// ## Using the Navigator API
1287///
1288/// Mobile apps typically reveal their contents via full-screen elements
1289/// called "screens" or "pages". In Flutter these elements are called
1290/// routes and they're managed by a [Navigator] widget. The navigator
1291/// manages a stack of [Route] objects and provides two ways for managing
1292/// the stack, the declarative API [Navigator.pages] or imperative API
1293/// [Navigator.push] and [Navigator.pop].
1294///
1295/// When your user interface fits this paradigm of a stack, where the user
1296/// should be able to _navigate_ back to an earlier element in the stack,
1297/// the use of routes and the Navigator is appropriate. On certain platforms,
1298/// such as Android, the system UI will provide a back button (outside the
1299/// bounds of your application) that will allow the user to navigate back
1300/// to earlier routes in your application's stack. On platforms that don't
1301/// have this build-in navigation mechanism, the use of an [AppBar] (typically
1302/// used in the [Scaffold.appBar] property) can automatically add a back
1303/// button for user navigation.
1304///
1305/// ### Displaying a full-screen route
1306///
1307/// Although you can create a navigator directly, it's most common to use the
1308/// navigator created by the `Router` which itself is created and configured by
1309/// a [WidgetsApp] or a [MaterialApp] widget. You can refer to that navigator
1310/// with [Navigator.of].
1311///
1312/// A [MaterialApp] is the simplest way to set things up. The [MaterialApp]'s
1313/// home becomes the route at the bottom of the [Navigator]'s stack. It is what
1314/// you see when the app is launched.
1315///
1316/// ```dart
1317/// void main() {
1318/// runApp(const MaterialApp(home: MyAppHome()));
1319/// }
1320/// ```
1321///
1322/// To push a new route on the stack you can create an instance of
1323/// [MaterialPageRoute] with a builder function that creates whatever you
1324/// want to appear on the screen. For example:
1325///
1326/// ```dart
1327/// Navigator.push(context, MaterialPageRoute<void>(
1328/// builder: (BuildContext context) {
1329/// return Scaffold(
1330/// appBar: AppBar(title: const Text('My Page')),
1331/// body: Center(
1332/// child: TextButton(
1333/// child: const Text('POP'),
1334/// onPressed: () {
1335/// Navigator.pop(context);
1336/// },
1337/// ),
1338/// ),
1339/// );
1340/// },
1341/// ));
1342/// ```
1343///
1344/// The route defines its widget with a builder function instead of a
1345/// child widget because it will be built and rebuilt in different
1346/// contexts depending on when it's pushed and popped.
1347///
1348/// As you can see, the new route can be popped, revealing the app's home
1349/// page, with the Navigator's pop method:
1350///
1351/// ```dart
1352/// Navigator.pop(context);
1353/// ```
1354///
1355/// It usually isn't necessary to provide a widget that pops the Navigator
1356/// in a route with a [Scaffold] because the Scaffold automatically adds a
1357/// 'back' button to its AppBar. Pressing the back button causes
1358/// [Navigator.pop] to be called. On Android, pressing the system back
1359/// button does the same thing.
1360///
1361/// ### Using named navigator routes
1362///
1363/// Mobile apps often manage a large number of routes and it's often
1364/// easiest to refer to them by name. Route names, by convention,
1365/// use a path-like structure (for example, '/a/b/c').
1366/// The app's home page route is named '/' by default.
1367///
1368/// The [MaterialApp] can be created
1369/// with a [Map<String, WidgetBuilder>] which maps from a route's name to
1370/// a builder function that will create it. The [MaterialApp] uses this
1371/// map to create a value for its navigator's [onGenerateRoute] callback.
1372///
1373/// ```dart
1374/// void main() {
1375/// runApp(MaterialApp(
1376/// home: const MyAppHome(), // becomes the route named '/'
1377/// routes: <String, WidgetBuilder> {
1378/// '/a': (BuildContext context) => const MyPage(title: Text('page A')),
1379/// '/b': (BuildContext context) => const MyPage(title: Text('page B')),
1380/// '/c': (BuildContext context) => const MyPage(title: Text('page C')),
1381/// },
1382/// ));
1383/// }
1384/// ```
1385///
1386/// To show a route by name:
1387///
1388/// ```dart
1389/// Navigator.pushNamed(context, '/b');
1390/// ```
1391///
1392/// ### Routes can return a value
1393///
1394/// When a route is pushed to ask the user for a value, the value can be
1395/// returned via the [pop] method's result parameter.
1396///
1397/// Methods that push a route return a [Future]. The Future resolves when the
1398/// route is popped and the [Future]'s value is the [pop] method's `result`
1399/// parameter.
1400///
1401/// For example if we wanted to ask the user to press 'OK' to confirm an
1402/// operation we could `await` the result of [Navigator.push]:
1403///
1404/// ```dart
1405/// bool? value = await Navigator.push(context, MaterialPageRoute<bool>(
1406/// builder: (BuildContext context) {
1407/// return Center(
1408/// child: GestureDetector(
1409/// child: const Text('OK'),
1410/// onTap: () { Navigator.pop(context, true); }
1411/// ),
1412/// );
1413/// }
1414/// ));
1415/// ```
1416///
1417/// If the user presses 'OK' then value will be true. If the user backs
1418/// out of the route, for example by pressing the Scaffold's back button,
1419/// the value will be null.
1420///
1421/// When a route is used to return a value, the route's type parameter must
1422/// match the type of [pop]'s result. That's why we've used
1423/// `MaterialPageRoute<bool>` instead of `MaterialPageRoute<void>` or just
1424/// `MaterialPageRoute`. (If you prefer to not specify the types, though, that's
1425/// fine too.)
1426///
1427/// ### Popup routes
1428///
1429/// Routes don't have to obscure the entire screen. [PopupRoute]s cover the
1430/// screen with a [ModalRoute.barrierColor] that can be only partially opaque to
1431/// allow the current screen to show through. Popup routes are "modal" because
1432/// they block input to the widgets below.
1433///
1434/// There are functions which create and show popup routes. For
1435/// example: [showDialog], [showMenu], and [showModalBottomSheet]. These
1436/// functions return their pushed route's Future as described above.
1437/// Callers can await the returned value to take an action when the
1438/// route is popped, or to discover the route's value.
1439///
1440/// There are also widgets which create popup routes, like [PopupMenuButton] and
1441/// [DropdownButton]. These widgets create internal subclasses of PopupRoute
1442/// and use the Navigator's push and pop methods to show and dismiss them.
1443///
1444/// ### Custom routes
1445///
1446/// You can create your own subclass of one of the widget library route classes
1447/// like [PopupRoute], [ModalRoute], or [PageRoute], to control the animated
1448/// transition employed to show the route, the color and behavior of the route's
1449/// modal barrier, and other aspects of the route.
1450///
1451/// The [PageRouteBuilder] class makes it possible to define a custom route
1452/// in terms of callbacks. Here's an example that rotates and fades its child
1453/// when the route appears or disappears. This route does not obscure the entire
1454/// screen because it specifies `opaque: false`, just as a popup route does.
1455///
1456/// ```dart
1457/// Navigator.push(context, PageRouteBuilder<void>(
1458/// opaque: false,
1459/// pageBuilder: (BuildContext context, _, _) {
1460/// return const Center(child: Text('My PageRoute'));
1461/// },
1462/// transitionsBuilder: (_, Animation<double> animation, _, Widget child) {
1463/// return FadeTransition(
1464/// opacity: animation,
1465/// child: RotationTransition(
1466/// turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
1467/// child: child,
1468/// ),
1469/// );
1470/// }
1471/// ));
1472/// ```
1473///
1474/// The page route is built in two parts, the "page" and the
1475/// "transitions". The page becomes a descendant of the child passed to
1476/// the `transitionsBuilder` function. Typically the page is only built once,
1477/// because it doesn't depend on its animation parameters (elided with `_`
1478/// in this example). The transition is built on every frame
1479/// for its duration.
1480///
1481/// (In this example, `void` is used as the return type for the route, because
1482/// it does not return a value.)
1483///
1484/// ### Nesting Navigators
1485///
1486/// An app can use more than one [Navigator]. Nesting one [Navigator] below
1487/// another [Navigator] can be used to create an "inner journey" such as tabbed
1488/// navigation, user registration, store checkout, or other independent journeys
1489/// that represent a subsection of your overall application.
1490///
1491/// #### Example
1492///
1493/// It is standard practice for iOS apps to use tabbed navigation where each
1494/// tab maintains its own navigation history. Therefore, each tab has its own
1495/// [Navigator], creating a kind of "parallel navigation."
1496///
1497/// In addition to the parallel navigation of the tabs, it is still possible to
1498/// launch full-screen pages that completely cover the tabs. For example: an
1499/// on-boarding flow, or an alert dialog. Therefore, there must exist a "root"
1500/// [Navigator] that sits above the tab navigation. As a result, each of the
1501/// tab's [Navigator]s are actually nested [Navigator]s sitting below a single
1502/// root [Navigator].
1503///
1504/// In practice, the nested [Navigator]s for tabbed navigation sit in the
1505/// [WidgetsApp] and [CupertinoTabView] widgets and do not need to be explicitly
1506/// created or managed.
1507///
1508/// {@tool sample}
1509/// The following example demonstrates how a nested [Navigator] can be used to
1510/// present a standalone user registration journey.
1511///
1512/// Even though this example uses two [Navigator]s to demonstrate nested
1513/// [Navigator]s, a similar result is possible using only a single [Navigator].
1514///
1515/// Run this example with `flutter run --route=/signup` to start it with
1516/// the signup flow instead of on the home page.
1517///
1518/// ** See code in examples/api/lib/widgets/navigator/navigator.0.dart **
1519/// {@end-tool}
1520///
1521/// [Navigator.of] operates on the nearest ancestor [Navigator] from the given
1522/// [BuildContext]. Be sure to provide a [BuildContext] below the intended
1523/// [Navigator], especially in large `build` methods where nested [Navigator]s
1524/// are created. The [Builder] widget can be used to access a [BuildContext] at
1525/// a desired location in the widget subtree.
1526///
1527/// ### Finding the enclosing route
1528///
1529/// In the common case of a modal route, the enclosing route can be obtained
1530/// from inside a build method using [ModalRoute.of]. To determine if the
1531/// enclosing route is the active route (e.g. so that controls can be dimmed
1532/// when the route is not active), the [Route.isCurrent] property can be checked
1533/// on the returned route.
1534///
1535/// ## State Restoration
1536///
1537/// If provided with a [restorationScopeId] and when surrounded by a valid
1538/// [RestorationScope] the [Navigator] will restore its state by recreating
1539/// the current history stack of [Route]s during state restoration and by
1540/// restoring the internal state of those [Route]s. However, not all [Route]s
1541/// on the stack can be restored:
1542///
1543/// * [Page]-based routes restore their state if [Page.restorationId] is
1544/// provided.
1545/// * [Route]s added with the classic imperative API ([push], [pushNamed], and
1546/// friends) can never restore their state.
1547/// * A [Route] added with the restorable imperative API ([restorablePush],
1548/// [restorablePushNamed], and all other imperative methods with "restorable"
1549/// in their name) restores its state if all routes below it up to and
1550/// including the first [Page]-based route below it are restored. If there
1551/// is no [Page]-based route below it, it only restores its state if all
1552/// routes below it restore theirs.
1553///
1554/// If a [Route] is deemed restorable, the [Navigator] will set its
1555/// [Route.restorationScopeId] to a non-null value. Routes can use that ID to
1556/// store and restore their own state. As an example, the [ModalRoute] will
1557/// use this ID to create a [RestorationScope] for its content widgets.
1558class Navigator extends StatefulWidget {
1559 /// Creates a widget that maintains a stack-based history of child widgets.
1560 ///
1561 /// If the [pages] is not empty, the [onPopPage] must not be null.
1562 const Navigator({
1563 super.key,
1564 this.pages = const <Page<dynamic>>[],
1565 @Deprecated(
1566 'Use onDidRemovePage instead. '
1567 'This feature was deprecated after v3.16.0-17.0.pre.',
1568 )
1569 this.onPopPage,
1570 this.initialRoute,
1571 this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
1572 this.onGenerateRoute,
1573 this.onUnknownRoute,
1574 this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
1575 this.reportsRouteUpdateToEngine = false,
1576 this.clipBehavior = Clip.hardEdge,
1577 this.observers = const <NavigatorObserver>[],
1578 this.requestFocus = true,
1579 this.restorationScopeId,
1580 this.routeTraversalEdgeBehavior = kDefaultRouteTraversalEdgeBehavior,
1581 this.routeDirectionalTraversalEdgeBehavior = kDefaultRouteDirectionalTraversalEdgeBehavior,
1582 this.onDidRemovePage,
1583 });
1584
1585 /// The list of pages with which to populate the history.
1586 ///
1587 /// Pages are turned into routes using [Page.createRoute] in a manner
1588 /// analogous to how [Widget]s are turned into [Element]s (and [State]s or
1589 /// [RenderObject]s) using [Widget.createElement] (and
1590 /// [StatefulWidget.createState] or [RenderObjectWidget.createRenderObject]).
1591 ///
1592 /// When this list is updated, the new list is compared to the previous
1593 /// list and the set of routes is updated accordingly.
1594 ///
1595 /// Some [Route]s do not correspond to [Page] objects, namely, those that are
1596 /// added to the history using the [Navigator] API ([push] and friends). A
1597 /// [Route] that does not correspond to a [Page] object is called a pageless
1598 /// route and is tied to the [Route] that _does_ correspond to a [Page] object
1599 /// that is below it in the history.
1600 ///
1601 /// Pages that are added or removed may be animated as controlled by the
1602 /// [transitionDelegate]. If a page is removed that had other pageless routes
1603 /// pushed on top of it using [push] and friends, those pageless routes are
1604 /// also removed with or without animation as determined by the
1605 /// [transitionDelegate].
1606 ///
1607 /// To use this API, an [onPopPage] callback must also be provided to properly
1608 /// clean up this list if a page has been popped.
1609 ///
1610 /// If [initialRoute] is non-null when the widget is first created, then
1611 /// [onGenerateInitialRoutes] is used to generate routes that are above those
1612 /// corresponding to [pages] in the initial history.
1613 final List<Page<dynamic>> pages;
1614
1615 /// This is deprecated and replaced by [onDidRemovePage].
1616 ///
1617 /// Called when [pop] is invoked but the current [Route] corresponds to a
1618 /// [Page] found in the [pages] list.
1619 ///
1620 /// The `result` argument is the value with which the route is to complete
1621 /// (e.g. the value returned from a dialog).
1622 ///
1623 /// This callback is responsible for calling [Route.didPop] and returning
1624 /// whether this pop is successful.
1625 ///
1626 /// The [Navigator] widget should be rebuilt with a [pages] list that does not
1627 /// contain the [Page] for the given [Route]. The next time the [pages] list
1628 /// is updated, if the [Page] corresponding to this [Route] is still present,
1629 /// it will be interpreted as a new route to display.
1630 @Deprecated(
1631 'Use onDidRemovePage instead. '
1632 'This feature was deprecated after v3.16.0-17.0.pre.',
1633 )
1634 final PopPageCallback? onPopPage;
1635
1636 /// Called when the [Route] associated with the given [Page] has been removed
1637 /// from the Navigator.
1638 ///
1639 /// This can happen when the route is removed or completed through
1640 /// [Navigator.pop], [Navigator.pushReplacement], or its friends.
1641 ///
1642 /// This callback is responsible for removing the given page from the list of
1643 /// [pages].
1644 ///
1645 /// The [Navigator] widget should be rebuilt with a [pages] list that does not
1646 /// contain the given page [Page]. The next time the [pages] list
1647 /// is updated, if the given [Page] is still present, it will be interpreted
1648 /// as a new page to display.
1649 final DidRemovePageCallback? onDidRemovePage;
1650
1651 /// The delegate used for deciding how routes transition in or off the screen
1652 /// during the [pages] updates.
1653 ///
1654 /// Defaults to [DefaultTransitionDelegate].
1655 final TransitionDelegate<dynamic> transitionDelegate;
1656
1657 /// The name of the first route to show.
1658 ///
1659 /// Defaults to [Navigator.defaultRouteName].
1660 ///
1661 /// The value is interpreted according to [onGenerateInitialRoutes], which
1662 /// defaults to [defaultGenerateInitialRoutes].
1663 ///
1664 /// Changing the [initialRoute] will have no effect, as it only controls the
1665 /// _initial_ route. To change the route while the application is running, use
1666 /// the static functions on this class, such as [push] or [replace].
1667 final String? initialRoute;
1668
1669 /// Called to generate a route for a given [RouteSettings].
1670 final RouteFactory? onGenerateRoute;
1671
1672 /// Called when [onGenerateRoute] fails to generate a route.
1673 ///
1674 /// This callback is typically used for error handling. For example, this
1675 /// callback might always generate a "not found" page that describes the route
1676 /// that wasn't found.
1677 ///
1678 /// Unknown routes can arise either from errors in the app or from external
1679 /// requests to push routes, such as from Android intents.
1680 final RouteFactory? onUnknownRoute;
1681
1682 /// A list of observers for this navigator.
1683 final List<NavigatorObserver> observers;
1684
1685 /// Restoration ID to save and restore the state of the navigator, including
1686 /// its history.
1687 ///
1688 /// {@template flutter.widgets.navigator.restorationScopeId}
1689 /// If a restoration ID is provided, the navigator will persist its internal
1690 /// state (including the route history as well as the restorable state of the
1691 /// routes) and restore it during state restoration.
1692 ///
1693 /// If no restoration ID is provided, the route history stack will not be
1694 /// restored and state restoration is disabled for the individual routes as
1695 /// well.
1696 ///
1697 /// The state is persisted in a [RestorationBucket] claimed from
1698 /// the surrounding [RestorationScope] using the provided restoration ID.
1699 /// Within that bucket, the [Navigator] also creates a new [RestorationScope]
1700 /// for its children (the [Route]s).
1701 ///
1702 /// See also:
1703 ///
1704 /// * [RestorationManager], which explains how state restoration works in
1705 /// Flutter.
1706 /// * [RestorationMixin], which contains a runnable code sample showcasing
1707 /// state restoration in Flutter.
1708 /// * [Navigator], which explains under the heading "state restoration"
1709 /// how and under what conditions the navigator restores its state.
1710 /// * [Navigator.restorablePush], which includes an example showcasing how
1711 /// to push a restorable route onto the navigator.
1712 /// {@endtemplate}
1713 final String? restorationScopeId;
1714
1715 /// Controls the transfer of focus beyond the first and the last items of a
1716 /// focus scope that defines focus traversal of widgets within a route.
1717 ///
1718 /// {@template flutter.widgets.navigator.routeTraversalEdgeBehavior}
1719 /// The focus inside routes installed in the top of the app affects how
1720 /// the app behaves with respect to the platform content surrounding it.
1721 /// For example, on the web, an app is at a minimum surrounded by browser UI,
1722 /// such as the address bar, browser tabs, and more. The user should be able
1723 /// to reach browser UI using normal focus shortcuts. Similarly, if the app
1724 /// is embedded within an `<iframe>` or inside a custom element, it should
1725 /// be able to participate in the overall focus traversal, including elements
1726 /// not rendered by Flutter.
1727 /// {@endtemplate}
1728 final TraversalEdgeBehavior routeTraversalEdgeBehavior;
1729
1730 /// Controls the directional transfer of focus beyond the first and the last
1731 /// items of a focus scope that defines focus traversal of widgets within a route.
1732 ///
1733 /// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
1734 final TraversalEdgeBehavior routeDirectionalTraversalEdgeBehavior;
1735
1736 /// The name for the default route of the application.
1737 ///
1738 /// See also:
1739 ///
1740 /// * [dart:ui.PlatformDispatcher.defaultRouteName], which reflects the route that the
1741 /// application was started with.
1742 static const String defaultRouteName = '/';
1743
1744 /// Called when the widget is created to generate the initial list of [Route]
1745 /// objects if [initialRoute] is not null.
1746 ///
1747 /// Defaults to [defaultGenerateInitialRoutes].
1748 ///
1749 /// The [NavigatorState] and [initialRoute] will be passed to the callback.
1750 /// The callback must return a list of [Route] objects with which the history
1751 /// will be primed.
1752 ///
1753 /// When parsing the initialRoute, if there's any chance that it may
1754 /// contain complex characters, it's best to use the
1755 /// [characters](https://pub.dev/packages/characters) API. This will ensure
1756 /// that extended grapheme clusters and surrogate pairs are treated as single
1757 /// characters by the code, the same way that they appear to the user. For
1758 /// example, the string "👨‍👩‍👦" appears to the user as a single
1759 /// character and `string.characters.length` intuitively returns 1. On the
1760 /// other hand, `string.length` returns 8, and `string.runes.length` returns
1761 /// 5!
1762 final RouteListFactory onGenerateInitialRoutes;
1763
1764 /// Whether this navigator should report route update message back to the
1765 /// engine when the top-most route changes.
1766 ///
1767 /// If the property is set to true, this navigator automatically sends the
1768 /// route update message to the engine when it detects top-most route changes.
1769 /// The messages are used by the web engine to update the browser URL bar.
1770 ///
1771 /// If the property is set to true when the [Navigator] is first created,
1772 /// single-entry history mode is requested using
1773 /// [SystemNavigator.selectSingleEntryHistory]. This means this property
1774 /// should not be used at the same time as [PlatformRouteInformationProvider]
1775 /// is used with a [Router] (including when used with [MaterialApp.router],
1776 /// for example).
1777 ///
1778 /// If there are multiple navigators in the widget tree, at most one of them
1779 /// can set this property to true (typically, the top-most one created from
1780 /// the [WidgetsApp]). Otherwise, the web engine may receive multiple route
1781 /// update messages from different navigators and fail to update the URL
1782 /// bar.
1783 ///
1784 /// Defaults to false.
1785 final bool reportsRouteUpdateToEngine;
1786
1787 /// {@macro flutter.material.Material.clipBehavior}
1788 ///
1789 /// In cases where clipping is not desired, consider setting this property to
1790 /// [Clip.none].
1791 ///
1792 /// Defaults to [Clip.hardEdge].
1793 final Clip clipBehavior;
1794
1795 /// Whether or not the navigator and it's new topmost route should request focus
1796 /// when the new route is pushed onto the navigator.
1797 ///
1798 /// If [Route.requestFocus] is set on the topmost route, that will take precedence
1799 /// over this value.
1800 ///
1801 /// Defaults to true.
1802 final bool requestFocus;
1803
1804 /// Push a named route onto the navigator that most tightly encloses the given
1805 /// context.
1806 ///
1807 /// {@template flutter.widgets.navigator.pushNamed}
1808 /// The route name will be passed to the [Navigator.onGenerateRoute]
1809 /// callback. The returned route will be pushed into the navigator.
1810 ///
1811 /// The new route and the previous route (if any) are notified (see
1812 /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
1813 /// [Navigator.observers], they will be notified as well (see
1814 /// [NavigatorObserver.didPush]).
1815 ///
1816 /// Ongoing gestures within the current route are canceled when a new route is
1817 /// pushed.
1818 ///
1819 /// The `T` type argument is the type of the return value of the route.
1820 ///
1821 /// To use [pushNamed], an [Navigator.onGenerateRoute] callback must be
1822 /// provided,
1823 /// {@endtemplate}
1824 ///
1825 /// {@template flutter.widgets.navigator.pushNamed.returnValue}
1826 /// Returns a [Future] that completes to the `result` value passed to [pop]
1827 /// when the pushed route is popped off the navigator.
1828 /// {@endtemplate}
1829 ///
1830 /// {@template flutter.widgets.Navigator.pushNamed}
1831 /// The provided `arguments` are passed to the pushed route via
1832 /// [RouteSettings.arguments]. Any object can be passed as `arguments` (e.g. a
1833 /// [String], [int], or an instance of a custom `MyRouteArguments` class).
1834 /// Often, a [Map] is used to pass key-value pairs.
1835 ///
1836 /// The `arguments` may be used in [Navigator.onGenerateRoute] or
1837 /// [Navigator.onUnknownRoute] to construct the route.
1838 /// {@endtemplate}
1839 ///
1840 /// {@tool snippet}
1841 ///
1842 /// Typical usage is as follows:
1843 ///
1844 /// ```dart
1845 /// void _didPushButton() {
1846 /// Navigator.pushNamed(context, '/settings');
1847 /// }
1848 /// ```
1849 /// {@end-tool}
1850 ///
1851 /// {@tool snippet}
1852 ///
1853 /// The following example shows how to pass additional `arguments` to the
1854 /// route:
1855 ///
1856 /// ```dart
1857 /// void _showBerlinWeather() {
1858 /// Navigator.pushNamed(
1859 /// context,
1860 /// '/weather',
1861 /// arguments: <String, String>{
1862 /// 'city': 'Berlin',
1863 /// 'country': 'Germany',
1864 /// },
1865 /// );
1866 /// }
1867 /// ```
1868 /// {@end-tool}
1869 ///
1870 /// {@tool snippet}
1871 ///
1872 /// The following example shows how to pass a custom Object to the route:
1873 ///
1874 /// ```dart
1875 /// class WeatherRouteArguments {
1876 /// WeatherRouteArguments({ required this.city, required this.country });
1877 /// final String city;
1878 /// final String country;
1879 ///
1880 /// bool get isGermanCapital {
1881 /// return country == 'Germany' && city == 'Berlin';
1882 /// }
1883 /// }
1884 ///
1885 /// void _showWeather() {
1886 /// Navigator.pushNamed(
1887 /// context,
1888 /// '/weather',
1889 /// arguments: WeatherRouteArguments(city: 'Berlin', country: 'Germany'),
1890 /// );
1891 /// }
1892 /// ```
1893 /// {@end-tool}
1894 ///
1895 /// See also:
1896 ///
1897 /// * [restorablePushNamed], which pushes a route that can be restored
1898 /// during state restoration.
1899 @optionalTypeArgs
1900 static Future<T?> pushNamed<T extends Object?>(
1901 BuildContext context,
1902 String routeName, {
1903 Object? arguments,
1904 }) {
1905 return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
1906 }
1907
1908 /// Push a named route onto the navigator that most tightly encloses the given
1909 /// context.
1910 ///
1911 /// {@template flutter.widgets.navigator.restorablePushNamed}
1912 /// Unlike [Route]s pushed via [pushNamed], [Route]s pushed with this method
1913 /// are restored during state restoration according to the rules outlined
1914 /// in the "State Restoration" section of [Navigator].
1915 /// {@endtemplate}
1916 ///
1917 /// {@macro flutter.widgets.navigator.pushNamed}
1918 ///
1919 /// {@template flutter.widgets.Navigator.restorablePushNamed.arguments}
1920 /// The provided `arguments` are passed to the pushed route via
1921 /// [RouteSettings.arguments]. Any object that is serializable via the
1922 /// [StandardMessageCodec] can be passed as `arguments`. Often, a Map is used
1923 /// to pass key-value pairs.
1924 ///
1925 /// The arguments may be used in [Navigator.onGenerateRoute] or
1926 /// [Navigator.onUnknownRoute] to construct the route.
1927 /// {@endtemplate}
1928 ///
1929 /// {@template flutter.widgets.Navigator.restorablePushNamed.returnValue}
1930 /// The method returns an opaque ID for the pushed route that can be used by
1931 /// the [RestorableRouteFuture] to gain access to the actual [Route] object
1932 /// added to the navigator and its return value. You can ignore the return
1933 /// value of this method, if you do not care about the route object or the
1934 /// route's return value.
1935 /// {@endtemplate}
1936 ///
1937 /// {@tool snippet}
1938 ///
1939 /// Typical usage is as follows:
1940 ///
1941 /// ```dart
1942 /// void _showParisWeather() {
1943 /// Navigator.restorablePushNamed(
1944 /// context,
1945 /// '/weather',
1946 /// arguments: <String, String>{
1947 /// 'city': 'Paris',
1948 /// 'country': 'France',
1949 /// },
1950 /// );
1951 /// }
1952 /// ```
1953 /// {@end-tool}
1954 @optionalTypeArgs
1955 static String restorablePushNamed<T extends Object?>(
1956 BuildContext context,
1957 String routeName, {
1958 Object? arguments,
1959 }) {
1960 return Navigator.of(context).restorablePushNamed<T>(routeName, arguments: arguments);
1961 }
1962
1963 /// Replace the current route of the navigator that most tightly encloses the
1964 /// given context by pushing the route named [routeName] and then disposing
1965 /// the previous route once the new route has finished animating in.
1966 ///
1967 /// {@template flutter.widgets.navigator.pushReplacementNamed}
1968 /// If non-null, `result` will be used as the result of the route that is
1969 /// removed; the future that had been returned from pushing that old route
1970 /// will complete with `result`. Routes such as dialogs or popup menus
1971 /// typically use this mechanism to return the value selected by the user to
1972 /// the widget that created their route. The type of `result`, if provided,
1973 /// must match the type argument of the class of the old route (`TO`).
1974 ///
1975 /// The route name will be passed to the [Navigator.onGenerateRoute]
1976 /// callback. The returned route will be pushed into the navigator.
1977 ///
1978 /// The new route and the route below the removed route are notified (see
1979 /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
1980 /// [Navigator.observers], they will be notified as well (see
1981 /// [NavigatorObserver.didReplace]). The removed route is notified once the
1982 /// new route has finished animating (see [Route.didComplete]). The removed
1983 /// route's exit animation is not run (see [popAndPushNamed] for a variant
1984 /// that animates the removed route).
1985 ///
1986 /// Ongoing gestures within the current route are canceled when a new route is
1987 /// pushed.
1988 ///
1989 /// The `T` type argument is the type of the return value of the new route,
1990 /// and `TO` is the type of the return value of the old route.
1991 ///
1992 /// To use [pushReplacementNamed], a [Navigator.onGenerateRoute] callback must
1993 /// be provided.
1994 /// {@endtemplate}
1995 ///
1996 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
1997 ///
1998 /// {@macro flutter.widgets.Navigator.pushNamed}
1999 ///
2000 /// {@tool snippet}
2001 ///
2002 /// Typical usage is as follows:
2003 ///
2004 /// ```dart
2005 /// void _switchToBrightness() {
2006 /// Navigator.pushReplacementNamed(context, '/settings/brightness');
2007 /// }
2008 /// ```
2009 /// {@end-tool}
2010 ///
2011 /// See also:
2012 ///
2013 /// * [restorablePushReplacementNamed], which pushes a replacement route that
2014 /// can be restored during state restoration.
2015 @optionalTypeArgs
2016 static Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
2017 BuildContext context,
2018 String routeName, {
2019 TO? result,
2020 Object? arguments,
2021 }) {
2022 return Navigator.of(
2023 context,
2024 ).pushReplacementNamed<T, TO>(routeName, arguments: arguments, result: result);
2025 }
2026
2027 /// Replace the current route of the navigator that most tightly encloses the
2028 /// given context by pushing the route named [routeName] and then disposing
2029 /// the previous route once the new route has finished animating in.
2030 ///
2031 /// {@template flutter.widgets.navigator.restorablePushReplacementNamed}
2032 /// Unlike [Route]s pushed via [pushReplacementNamed], [Route]s pushed with
2033 /// this method are restored during state restoration according to the rules
2034 /// outlined in the "State Restoration" section of [Navigator].
2035 /// {@endtemplate}
2036 ///
2037 /// {@macro flutter.widgets.navigator.pushReplacementNamed}
2038 ///
2039 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
2040 ///
2041 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2042 ///
2043 /// {@tool snippet}
2044 ///
2045 /// Typical usage is as follows:
2046 ///
2047 /// ```dart
2048 /// void _switchToAudioVolume() {
2049 /// Navigator.restorablePushReplacementNamed(context, '/settings/volume');
2050 /// }
2051 /// ```
2052 /// {@end-tool}
2053 @optionalTypeArgs
2054 static String restorablePushReplacementNamed<T extends Object?, TO extends Object?>(
2055 BuildContext context,
2056 String routeName, {
2057 TO? result,
2058 Object? arguments,
2059 }) {
2060 return Navigator.of(
2061 context,
2062 ).restorablePushReplacementNamed<T, TO>(routeName, arguments: arguments, result: result);
2063 }
2064
2065 /// Pop the current route off the navigator that most tightly encloses the
2066 /// given context and push a named route in its place.
2067 ///
2068 /// {@template flutter.widgets.navigator.popAndPushNamed}
2069 /// The popping of the previous route is handled as per [pop].
2070 ///
2071 /// The new route's name will be passed to the [Navigator.onGenerateRoute]
2072 /// callback. The returned route will be pushed into the navigator.
2073 ///
2074 /// The new route, the old route, and the route below the old route (if any)
2075 /// are all notified (see [Route.didPop], [Route.didComplete],
2076 /// [Route.didPopNext], [Route.didPush], and [Route.didChangeNext]). If the
2077 /// [Navigator] has any [Navigator.observers], they will be notified as well
2078 /// (see [NavigatorObserver.didPop] and [NavigatorObserver.didPush]). The
2079 /// animations for the pop and the push are performed simultaneously, so the
2080 /// route below may be briefly visible even if both the old route and the new
2081 /// route are opaque (see [TransitionRoute.opaque]).
2082 ///
2083 /// Ongoing gestures within the current route are canceled when a new route is
2084 /// pushed.
2085 ///
2086 /// The `T` type argument is the type of the return value of the new route,
2087 /// and `TO` is the return value type of the old route.
2088 ///
2089 /// To use [popAndPushNamed], a [Navigator.onGenerateRoute] callback must be provided.
2090 /// {@endtemplate}
2091 ///
2092 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
2093 ///
2094 /// {@macro flutter.widgets.Navigator.pushNamed}
2095 ///
2096 /// {@tool snippet}
2097 ///
2098 /// Typical usage is as follows:
2099 ///
2100 /// ```dart
2101 /// void _selectAccessibility() {
2102 /// Navigator.popAndPushNamed(context, '/settings/accessibility');
2103 /// }
2104 /// ```
2105 /// {@end-tool}
2106 ///
2107 /// See also:
2108 ///
2109 /// * [restorablePopAndPushNamed], which pushes a new route that can be
2110 /// restored during state restoration.
2111 @optionalTypeArgs
2112 static Future<T?> popAndPushNamed<T extends Object?, TO extends Object?>(
2113 BuildContext context,
2114 String routeName, {
2115 TO? result,
2116 Object? arguments,
2117 }) {
2118 return Navigator.of(
2119 context,
2120 ).popAndPushNamed<T, TO>(routeName, arguments: arguments, result: result);
2121 }
2122
2123 /// Pop the current route off the navigator that most tightly encloses the
2124 /// given context and push a named route in its place.
2125 ///
2126 /// {@template flutter.widgets.navigator.restorablePopAndPushNamed}
2127 /// Unlike [Route]s pushed via [popAndPushNamed], [Route]s pushed with
2128 /// this method are restored during state restoration according to the rules
2129 /// outlined in the "State Restoration" section of [Navigator].
2130 /// {@endtemplate}
2131 ///
2132 /// {@macro flutter.widgets.navigator.popAndPushNamed}
2133 ///
2134 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
2135 ///
2136 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2137 ///
2138 /// {@tool snippet}
2139 ///
2140 /// Typical usage is as follows:
2141 ///
2142 /// ```dart
2143 /// void _selectNetwork() {
2144 /// Navigator.restorablePopAndPushNamed(context, '/settings/network');
2145 /// }
2146 /// ```
2147 /// {@end-tool}
2148 @optionalTypeArgs
2149 static String restorablePopAndPushNamed<T extends Object?, TO extends Object?>(
2150 BuildContext context,
2151 String routeName, {
2152 TO? result,
2153 Object? arguments,
2154 }) {
2155 return Navigator.of(
2156 context,
2157 ).restorablePopAndPushNamed<T, TO>(routeName, arguments: arguments, result: result);
2158 }
2159
2160 /// Push the route with the given name onto the navigator that most tightly
2161 /// encloses the given context, and then remove all the previous routes until
2162 /// the `predicate` returns true.
2163 ///
2164 /// {@template flutter.widgets.navigator.pushNamedAndRemoveUntil}
2165 /// The predicate may be applied to the same route more than once if
2166 /// [Route.willHandlePopInternally] is true.
2167 ///
2168 /// To remove routes until a route with a certain name, use the
2169 /// [RoutePredicate] returned from [ModalRoute.withName].
2170 ///
2171 /// To remove all the routes below the pushed route, use a [RoutePredicate]
2172 /// that always returns false (e.g. `(Route<dynamic> route) => false`).
2173 ///
2174 /// The removed routes are removed without being completed, so this method
2175 /// does not take a return value argument.
2176 ///
2177 /// The new route's name (`routeName`) will be passed to the
2178 /// [Navigator.onGenerateRoute] callback. The returned route will be pushed
2179 /// into the navigator.
2180 ///
2181 /// The new route and the route below the bottommost removed route (which
2182 /// becomes the route below the new route) are notified (see [Route.didPush]
2183 /// and [Route.didChangeNext]). If the [Navigator] has any
2184 /// [Navigator.observers], they will be notified as well (see
2185 /// [NavigatorObserver.didPush] and [NavigatorObserver.didRemove]). The
2186 /// removed routes are disposed, once the new route has finished animating,
2187 /// and the futures that had been returned from pushing those routes
2188 /// will complete.
2189 ///
2190 /// Ongoing gestures within the current route are canceled when a new route is
2191 /// pushed.
2192 ///
2193 /// The `T` type argument is the type of the return value of the new route.
2194 ///
2195 /// To use [pushNamedAndRemoveUntil], an [Navigator.onGenerateRoute] callback
2196 /// must be provided.
2197 /// {@endtemplate}
2198 ///
2199 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
2200 ///
2201 /// {@macro flutter.widgets.Navigator.pushNamed}
2202 ///
2203 /// {@tool snippet}
2204 ///
2205 /// Typical usage is as follows:
2206 ///
2207 /// ```dart
2208 /// void _resetToCalendar() {
2209 /// Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/'));
2210 /// }
2211 /// ```
2212 /// {@end-tool}
2213 ///
2214 /// See also:
2215 ///
2216 /// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can
2217 /// be restored during state restoration.
2218 @optionalTypeArgs
2219 static Future<T?> pushNamedAndRemoveUntil<T extends Object?>(
2220 BuildContext context,
2221 String newRouteName,
2222 RoutePredicate predicate, {
2223 Object? arguments,
2224 }) {
2225 return Navigator.of(
2226 context,
2227 ).pushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
2228 }
2229
2230 /// Push the route with the given name onto the navigator that most tightly
2231 /// encloses the given context, and then remove all the previous routes until
2232 /// the `predicate` returns true.
2233 ///
2234 /// {@template flutter.widgets.navigator.restorablePushNamedAndRemoveUntil}
2235 /// Unlike [Route]s pushed via [pushNamedAndRemoveUntil], [Route]s pushed with
2236 /// this method are restored during state restoration according to the rules
2237 /// outlined in the "State Restoration" section of [Navigator].
2238 /// {@endtemplate}
2239 ///
2240 /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil}
2241 ///
2242 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
2243 ///
2244 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2245 ///
2246 /// {@tool snippet}
2247 ///
2248 /// Typical usage is as follows:
2249 ///
2250 /// ```dart
2251 /// void _resetToOverview() {
2252 /// Navigator.restorablePushNamedAndRemoveUntil(context, '/overview', ModalRoute.withName('/'));
2253 /// }
2254 /// ```
2255 /// {@end-tool}
2256 @optionalTypeArgs
2257 static String restorablePushNamedAndRemoveUntil<T extends Object?>(
2258 BuildContext context,
2259 String newRouteName,
2260 RoutePredicate predicate, {
2261 Object? arguments,
2262 }) {
2263 return Navigator.of(
2264 context,
2265 ).restorablePushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
2266 }
2267
2268 /// Push the given route onto the navigator that most tightly encloses the
2269 /// given context.
2270 ///
2271 /// {@template flutter.widgets.navigator.push}
2272 /// The new route and the previous route (if any) are notified (see
2273 /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
2274 /// [Navigator.observers], they will be notified as well (see
2275 /// [NavigatorObserver.didPush]).
2276 ///
2277 /// Ongoing gestures within the current route are canceled when a new route is
2278 /// pushed.
2279 ///
2280 /// The `T` type argument is the type of the return value of the route.
2281 /// {@endtemplate}
2282 ///
2283 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
2284 ///
2285 /// {@tool snippet}
2286 ///
2287 /// Typical usage is as follows:
2288 ///
2289 /// ```dart
2290 /// void _openMyPage() {
2291 /// Navigator.push<void>(
2292 /// context,
2293 /// MaterialPageRoute<void>(
2294 /// builder: (BuildContext context) => const MyPage(),
2295 /// ),
2296 /// );
2297 /// }
2298 /// ```
2299 /// {@end-tool}
2300 ///
2301 /// See also:
2302 ///
2303 /// * [restorablePush], which pushes a route that can be restored during
2304 /// state restoration.
2305 @optionalTypeArgs
2306 static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
2307 return Navigator.of(context).push(route);
2308 }
2309
2310 /// Push a new route onto the navigator that most tightly encloses the
2311 /// given context.
2312 ///
2313 /// {@template flutter.widgets.navigator.restorablePush}
2314 /// Unlike [Route]s pushed via [push], [Route]s pushed with this method are
2315 /// restored during state restoration according to the rules outlined in the
2316 /// "State Restoration" section of [Navigator].
2317 /// {@endtemplate}
2318 ///
2319 /// {@macro flutter.widgets.navigator.push}
2320 ///
2321 /// {@template flutter.widgets.Navigator.restorablePush}
2322 /// The method takes a [RestorableRouteBuilder] as argument, which must be a
2323 /// _static_ function annotated with `@pragma('vm:entry-point')`. It must
2324 /// instantiate and return a new [Route] object that will be added to the
2325 /// navigator. The provided `arguments` object is passed to the
2326 /// `routeBuilder`. The navigator calls the static `routeBuilder` function
2327 /// again during state restoration to re-create the route object.
2328 ///
2329 /// Any object that is serializable via the [StandardMessageCodec] can be
2330 /// passed as `arguments`. Often, a Map is used to pass key-value pairs.
2331 /// {@endtemplate}
2332 ///
2333 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2334 ///
2335 /// {@tool dartpad}
2336 /// Typical usage is as follows:
2337 ///
2338 /// ** See code in examples/api/lib/widgets/navigator/navigator.restorable_push.0.dart **
2339 /// {@end-tool}
2340 @optionalTypeArgs
2341 static String restorablePush<T extends Object?>(
2342 BuildContext context,
2343 RestorableRouteBuilder<T> routeBuilder, {
2344 Object? arguments,
2345 }) {
2346 return Navigator.of(context).restorablePush(routeBuilder, arguments: arguments);
2347 }
2348
2349 /// Replace the current route of the navigator that most tightly encloses the
2350 /// given context by pushing the given route and then disposing the previous
2351 /// route once the new route has finished animating in.
2352 ///
2353 /// {@template flutter.widgets.navigator.pushReplacement}
2354 /// If non-null, `result` will be used as the result of the route that is
2355 /// removed; the future that had been returned from pushing that old route will
2356 /// complete with `result`. Routes such as dialogs or popup menus typically
2357 /// use this mechanism to return the value selected by the user to the widget
2358 /// that created their route. The type of `result`, if provided, must match
2359 /// the type argument of the class of the old route (`TO`).
2360 ///
2361 /// The new route and the route below the removed route are notified (see
2362 /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
2363 /// [Navigator.observers], they will be notified as well (see
2364 /// [NavigatorObserver.didReplace]). The removed route is notified once the
2365 /// new route has finished animating (see [Route.didComplete]).
2366 ///
2367 /// Ongoing gestures within the current route are canceled when a new route is
2368 /// pushed.
2369 ///
2370 /// The `T` type argument is the type of the return value of the new route,
2371 /// and `TO` is the type of the return value of the old route.
2372 /// {@endtemplate}
2373 ///
2374 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
2375 ///
2376 /// {@tool snippet}
2377 ///
2378 /// Typical usage is as follows:
2379 ///
2380 /// ```dart
2381 /// void _completeLogin() {
2382 /// Navigator.pushReplacement<void, void>(
2383 /// context,
2384 /// MaterialPageRoute<void>(
2385 /// builder: (BuildContext context) => const MyHomePage(),
2386 /// ),
2387 /// );
2388 /// }
2389 /// ```
2390 /// {@end-tool}
2391 ///
2392 /// See also:
2393 ///
2394 /// * [restorablePushReplacement], which pushes a replacement route that can
2395 /// be restored during state restoration.
2396 @optionalTypeArgs
2397 static Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
2398 BuildContext context,
2399 Route<T> newRoute, {
2400 TO? result,
2401 }) {
2402 return Navigator.of(context).pushReplacement<T, TO>(newRoute, result: result);
2403 }
2404
2405 /// Replace the current route of the navigator that most tightly encloses the
2406 /// given context by pushing a new route and then disposing the previous
2407 /// route once the new route has finished animating in.
2408 ///
2409 /// {@template flutter.widgets.navigator.restorablePushReplacement}
2410 /// Unlike [Route]s pushed via [pushReplacement], [Route]s pushed with this
2411 /// method are restored during state restoration according to the rules
2412 /// outlined in the "State Restoration" section of [Navigator].
2413 /// {@endtemplate}
2414 ///
2415 /// {@macro flutter.widgets.navigator.pushReplacement}
2416 ///
2417 /// {@macro flutter.widgets.Navigator.restorablePush}
2418 ///
2419 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2420 ///
2421 /// {@tool dartpad}
2422 /// Typical usage is as follows:
2423 ///
2424 /// ** See code in examples/api/lib/widgets/navigator/navigator.restorable_push_replacement.0.dart **
2425 /// {@end-tool}
2426 @optionalTypeArgs
2427 static String restorablePushReplacement<T extends Object?, TO extends Object?>(
2428 BuildContext context,
2429 RestorableRouteBuilder<T> routeBuilder, {
2430 TO? result,
2431 Object? arguments,
2432 }) {
2433 return Navigator.of(
2434 context,
2435 ).restorablePushReplacement<T, TO>(routeBuilder, result: result, arguments: arguments);
2436 }
2437
2438 /// Push the given route onto the navigator that most tightly encloses the
2439 /// given context, and then remove all the previous routes until the
2440 /// `predicate` returns true.
2441 ///
2442 /// {@template flutter.widgets.navigator.pushAndRemoveUntil}
2443 /// The predicate may be applied to the same route more than once if
2444 /// [Route.willHandlePopInternally] is true.
2445 ///
2446 /// To remove routes until a route with a certain name, use the
2447 /// [RoutePredicate] returned from [ModalRoute.withName].
2448 ///
2449 /// To remove all the routes below the pushed route, use a [RoutePredicate]
2450 /// that always returns false (e.g. `(Route<dynamic> route) => false`).
2451 ///
2452 /// The removed routes are removed without being completed, so this method
2453 /// does not take a return value argument.
2454 ///
2455 /// The newly pushed route and its preceding route are notified for
2456 /// [Route.didPush]. After removal, the new route and its new preceding route,
2457 /// (the route below the bottommost removed route) are notified through
2458 /// [Route.didChangeNext]). If the [Navigator] has any [Navigator.observers],
2459 /// they will be notified as well (see [NavigatorObserver.didPush] and
2460 /// [NavigatorObserver.didRemove]). The removed routes are disposed of and
2461 /// notified, once the new route has finished animating. The futures that had
2462 /// been returned from pushing those routes will complete.
2463 ///
2464 /// Ongoing gestures within the current route are canceled when a new route is
2465 /// pushed.
2466 ///
2467 /// The `T` type argument is the type of the return value of the new route.
2468 /// {@endtemplate}
2469 ///
2470 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
2471 ///
2472 /// {@tool snippet}
2473 ///
2474 /// Typical usage is as follows:
2475 ///
2476 /// ```dart
2477 /// void _finishAccountCreation() {
2478 /// Navigator.pushAndRemoveUntil<void>(
2479 /// context,
2480 /// MaterialPageRoute<void>(builder: (BuildContext context) => const MyHomePage()),
2481 /// ModalRoute.withName('/'),
2482 /// );
2483 /// }
2484 /// ```
2485 /// {@end-tool}
2486 ///
2487 /// See also:
2488 ///
2489 /// * [restorablePushAndRemoveUntil], which pushes a route that can be
2490 /// restored during state restoration.
2491 @optionalTypeArgs
2492 static Future<T?> pushAndRemoveUntil<T extends Object?>(
2493 BuildContext context,
2494 Route<T> newRoute,
2495 RoutePredicate predicate,
2496 ) {
2497 return Navigator.of(context).pushAndRemoveUntil<T>(newRoute, predicate);
2498 }
2499
2500 /// Push a new route onto the navigator that most tightly encloses the
2501 /// given context, and then remove all the previous routes until the
2502 /// `predicate` returns true.
2503 ///
2504 /// {@template flutter.widgets.navigator.restorablePushAndRemoveUntil}
2505 /// Unlike [Route]s pushed via [pushAndRemoveUntil], [Route]s pushed with this
2506 /// method are restored during state restoration according to the rules
2507 /// outlined in the "State Restoration" section of [Navigator].
2508 /// {@endtemplate}
2509 ///
2510 /// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
2511 ///
2512 /// {@macro flutter.widgets.Navigator.restorablePush}
2513 ///
2514 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2515 ///
2516 /// {@tool dartpad}
2517 /// Typical usage is as follows:
2518 ///
2519 /// ** See code in examples/api/lib/widgets/navigator/navigator.restorable_push_and_remove_until.0.dart **
2520 /// {@end-tool}
2521 @optionalTypeArgs
2522 static String restorablePushAndRemoveUntil<T extends Object?>(
2523 BuildContext context,
2524 RestorableRouteBuilder<T> newRouteBuilder,
2525 RoutePredicate predicate, {
2526 Object? arguments,
2527 }) {
2528 return Navigator.of(
2529 context,
2530 ).restorablePushAndRemoveUntil<T>(newRouteBuilder, predicate, arguments: arguments);
2531 }
2532
2533 /// Replaces a route on the navigator that most tightly encloses the given
2534 /// context with a new route.
2535 ///
2536 /// {@template flutter.widgets.navigator.replace}
2537 /// The old route must not be currently visible, as this method skips the
2538 /// animations and therefore the removal would be jarring if it was visible.
2539 /// To replace the top-most route, consider [pushReplacement] instead, which
2540 /// _does_ animate the new route, and delays removing the old route until the
2541 /// new route has finished animating.
2542 ///
2543 /// The removed route is removed and completed with a `null` value.
2544 ///
2545 /// The new route, the route below the new route (if any), and the route above
2546 /// the new route, are all notified (see [Route.didReplace],
2547 /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
2548 /// has any [Navigator.observers], they will be notified as well (see
2549 /// [NavigatorObserver.didReplace]). The removed route is disposed with its
2550 /// future completed.
2551 ///
2552 /// This can be useful in combination with [removeRouteBelow] when building a
2553 /// non-linear user experience.
2554 ///
2555 /// The `T` type argument is the type of the return value of the new route.
2556 /// {@endtemplate}
2557 ///
2558 /// See also:
2559 ///
2560 /// * [replaceRouteBelow], which is the same but identifies the route to be
2561 /// removed by reference to the route above it, rather than directly.
2562 /// * [restorableReplace], which adds a replacement route that can be
2563 /// restored during state restoration.
2564 @optionalTypeArgs
2565 static void replace<T extends Object?>(
2566 BuildContext context, {
2567 required Route<dynamic> oldRoute,
2568 required Route<T> newRoute,
2569 }) {
2570 return Navigator.of(context).replace<T>(oldRoute: oldRoute, newRoute: newRoute);
2571 }
2572
2573 /// Replaces a route on the navigator that most tightly encloses the given
2574 /// context with a new route.
2575 ///
2576 /// {@template flutter.widgets.navigator.restorableReplace}
2577 /// Unlike [Route]s added via [replace], [Route]s added with this method are
2578 /// restored during state restoration according to the rules outlined in the
2579 /// "State Restoration" section of [Navigator].
2580 /// {@endtemplate}
2581 ///
2582 /// {@macro flutter.widgets.navigator.replace}
2583 ///
2584 /// {@macro flutter.widgets.Navigator.restorablePush}
2585 ///
2586 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2587 @optionalTypeArgs
2588 static String restorableReplace<T extends Object?>(
2589 BuildContext context, {
2590 required Route<dynamic> oldRoute,
2591 required RestorableRouteBuilder<T> newRouteBuilder,
2592 Object? arguments,
2593 }) {
2594 return Navigator.of(context).restorableReplace<T>(
2595 oldRoute: oldRoute,
2596 newRouteBuilder: newRouteBuilder,
2597 arguments: arguments,
2598 );
2599 }
2600
2601 /// Replaces a route on the navigator that most tightly encloses the given
2602 /// context with a new route. The route to be replaced is the one below the
2603 /// given `anchorRoute`.
2604 ///
2605 /// {@template flutter.widgets.navigator.replaceRouteBelow}
2606 /// The old route must not be current visible, as this method skips the
2607 /// animations and therefore the removal would be jarring if it was visible.
2608 /// To replace the top-most route, consider [pushReplacement] instead, which
2609 /// _does_ animate the new route, and delays removing the old route until the
2610 /// new route has finished animating.
2611 ///
2612 /// The removed route is removed and completed with a `null` value.
2613 ///
2614 /// The new route, the route below the new route (if any), and the route above
2615 /// the new route, are all notified (see [Route.didReplace],
2616 /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
2617 /// has any [Navigator.observers], they will be notified as well (see
2618 /// [NavigatorObserver.didReplace]). The removed route is disposed with its
2619 /// future completed.
2620 ///
2621 /// The `T` type argument is the type of the return value of the new route.
2622 /// {@endtemplate}
2623 ///
2624 /// See also:
2625 ///
2626 /// * [replace], which is the same but identifies the route to be removed
2627 /// directly.
2628 /// * [restorableReplaceRouteBelow], which adds a replacement route that can
2629 /// be restored during state restoration.
2630 @optionalTypeArgs
2631 static void replaceRouteBelow<T extends Object?>(
2632 BuildContext context, {
2633 required Route<dynamic> anchorRoute,
2634 required Route<T> newRoute,
2635 }) {
2636 return Navigator.of(context).replaceRouteBelow<T>(anchorRoute: anchorRoute, newRoute: newRoute);
2637 }
2638
2639 /// Replaces a route on the navigator that most tightly encloses the given
2640 /// context with a new route. The route to be replaced is the one below the
2641 /// given `anchorRoute`.
2642 ///
2643 /// {@template flutter.widgets.navigator.restorableReplaceRouteBelow}
2644 /// Unlike [Route]s added via [restorableReplaceRouteBelow], [Route]s added
2645 /// with this method are restored during state restoration according to the
2646 /// rules outlined in the "State Restoration" section of [Navigator].
2647 /// {@endtemplate}
2648 ///
2649 /// {@macro flutter.widgets.navigator.replaceRouteBelow}
2650 ///
2651 /// {@macro flutter.widgets.Navigator.restorablePush}
2652 ///
2653 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
2654 @optionalTypeArgs
2655 static String restorableReplaceRouteBelow<T extends Object?>(
2656 BuildContext context, {
2657 required Route<dynamic> anchorRoute,
2658 required RestorableRouteBuilder<T> newRouteBuilder,
2659 Object? arguments,
2660 }) {
2661 return Navigator.of(context).restorableReplaceRouteBelow<T>(
2662 anchorRoute: anchorRoute,
2663 newRouteBuilder: newRouteBuilder,
2664 arguments: arguments,
2665 );
2666 }
2667
2668 /// Whether the navigator that most tightly encloses the given context can be
2669 /// popped.
2670 ///
2671 /// {@template flutter.widgets.navigator.canPop}
2672 /// The initial route cannot be popped off the navigator, which implies that
2673 /// this function returns true only if popping the navigator would not remove
2674 /// the initial route.
2675 ///
2676 /// If there is no [Navigator] in scope, returns false.
2677 ///
2678 /// Does not consider anything that might externally prevent popping, such as
2679 /// [PopEntry].
2680 /// {@endtemplate}
2681 ///
2682 /// See also:
2683 ///
2684 /// * [Route.isFirst], which returns true for routes for which [canPop]
2685 /// returns false.
2686 static bool canPop(BuildContext context) {
2687 final NavigatorState? navigator = Navigator.maybeOf(context);
2688 return navigator != null && navigator.canPop();
2689 }
2690
2691 /// Consults the current route's [Route.popDisposition] getter or
2692 /// [Route.willPop] method, and acts accordingly, potentially popping the
2693 /// route as a result; returns whether the pop request should be considered
2694 /// handled.
2695 ///
2696 /// {@template flutter.widgets.navigator.maybePop}
2697 /// If the [RoutePopDisposition] is [RoutePopDisposition.pop], then the [pop]
2698 /// method is called, and this method returns true, indicating that it handled
2699 /// the pop request.
2700 ///
2701 /// If the [RoutePopDisposition] is [RoutePopDisposition.doNotPop], then this
2702 /// method returns true, but does not do anything beyond that.
2703 ///
2704 /// If the [RoutePopDisposition] is [RoutePopDisposition.bubble], then this
2705 /// method returns false, and the caller is responsible for sending the
2706 /// request to the containing scope (e.g. by closing the application).
2707 ///
2708 /// This method is typically called for a user-initiated [pop]. For example on
2709 /// Android it's called by the binding for the system's back button.
2710 ///
2711 /// The `T` type argument is the type of the return value of the current
2712 /// route. (Typically this isn't known; consider specifying `dynamic` or
2713 /// `Null`.)
2714 /// {@endtemplate}
2715 ///
2716 /// See also:
2717 ///
2718 /// * [Form], which provides an `onWillPop` callback that enables the form
2719 /// to veto a [pop] initiated by the app's back button.
2720 /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
2721 /// to define the route's `willPop` method.
2722 @optionalTypeArgs
2723 static Future<bool> maybePop<T extends Object?>(BuildContext context, [T? result]) {
2724 return Navigator.of(context).maybePop<T>(result);
2725 }
2726
2727 /// Pop the top-most route off the navigator that most tightly encloses the
2728 /// given context.
2729 ///
2730 /// {@template flutter.widgets.navigator.pop}
2731 /// The current route's [Route.didPop] method is called first. If that method
2732 /// returns false, then the route remains in the [Navigator]'s history (the
2733 /// route is expected to have popped some internal state; see e.g.
2734 /// [LocalHistoryRoute]). Otherwise, the rest of this description applies.
2735 ///
2736 /// If non-null, `result` will be used as the result of the route that is
2737 /// popped; the future that had been returned from pushing the popped route
2738 /// will complete with `result`. Routes such as dialogs or popup menus
2739 /// typically use this mechanism to return the value selected by the user to
2740 /// the widget that created their route. The type of `result`, if provided,
2741 /// must match the type argument of the class of the popped route (`T`).
2742 ///
2743 /// The popped route and the route below it are notified (see [Route.didPop],
2744 /// [Route.didComplete], and [Route.didPopNext]). If the [Navigator] has any
2745 /// [Navigator.observers], they will be notified as well (see
2746 /// [NavigatorObserver.didPop]).
2747 ///
2748 /// The `T` type argument is the type of the return value of the popped route.
2749 ///
2750 /// The type of `result`, if provided, must match the type argument of the
2751 /// class of the popped route (`T`).
2752 /// {@endtemplate}
2753 ///
2754 /// {@tool snippet}
2755 ///
2756 /// Typical usage for closing a route is as follows:
2757 ///
2758 /// ```dart
2759 /// void _close() {
2760 /// Navigator.pop(context);
2761 /// }
2762 /// ```
2763 /// {@end-tool}
2764 ///
2765 /// A dialog box might be closed with a result:
2766 ///
2767 /// ```dart
2768 /// void _accept() {
2769 /// Navigator.pop(context, true); // dialog returns true
2770 /// }
2771 /// ```
2772 @optionalTypeArgs
2773 static void pop<T extends Object?>(BuildContext context, [T? result]) {
2774 Navigator.of(context).pop<T>(result);
2775 }
2776
2777 /// Calls [pop] repeatedly on the navigator that most tightly encloses the
2778 /// given context until the predicate returns true.
2779 ///
2780 /// {@template flutter.widgets.navigator.popUntil}
2781 /// The predicate may be applied to the same route more than once if
2782 /// [Route.willHandlePopInternally] is true.
2783 ///
2784 /// To pop until a route with a certain name, use the [RoutePredicate]
2785 /// returned from [ModalRoute.withName].
2786 ///
2787 /// The routes are closed with null as their `return` value.
2788 ///
2789 /// See [pop] for more details of the semantics of popping a route.
2790 /// {@endtemplate}
2791 ///
2792 /// {@tool snippet}
2793 ///
2794 /// Typical usage is as follows:
2795 ///
2796 /// ```dart
2797 /// void _logout() {
2798 /// Navigator.popUntil(context, ModalRoute.withName('/login'));
2799 /// }
2800 /// ```
2801 /// {@end-tool}
2802 static void popUntil(BuildContext context, RoutePredicate predicate) {
2803 Navigator.of(context).popUntil(predicate);
2804 }
2805
2806 /// Immediately remove `route` from the navigator that most tightly encloses
2807 /// the given context, and [Route.dispose] it.
2808 ///
2809 /// {@template flutter.widgets.navigator.removeRoute}
2810 /// No animations are run as a result of this method call.
2811 ///
2812 /// The routes below and above the removed route are notified (see
2813 /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator]
2814 /// has any [Navigator.observers], they will be notified as well (see
2815 /// [NavigatorObserver.didRemove]). The removed route is disposed with its
2816 /// future completed.
2817 ///
2818 /// The given `route` must be in the history; this method will throw an
2819 /// exception if it is not.
2820 ///
2821 /// If non-null, `result` will be used as the result of the route that is
2822 /// removed; the future that had been returned from pushing the removed route
2823 /// will complete with `result`. If provided, must match the type argument of
2824 /// the class of the popped route (`T`).
2825 ///
2826 /// The `T` type argument is the type of the return value of the popped route.
2827 ///
2828 /// The type of `result`, if provided, must match the type argument of the
2829 /// class of the removed route (`T`).
2830 ///
2831 /// Ongoing gestures within the current route are canceled.
2832 /// {@endtemplate}
2833 ///
2834 /// This method is used, for example, to instantly dismiss dropdown menus that
2835 /// are up when the screen's orientation changes.
2836 @optionalTypeArgs
2837 static void removeRoute<T extends Object?>(BuildContext context, Route<T> route, [T? result]) {
2838 return Navigator.of(context).removeRoute<T>(route, result);
2839 }
2840
2841 /// Immediately remove a route from the navigator that most tightly encloses
2842 /// the given context, and [Route.dispose] it. The route to be removed is the
2843 /// one below the given `anchorRoute`.
2844 ///
2845 /// {@template flutter.widgets.navigator.removeRouteBelow}
2846 /// No animations are run as a result of this method call.
2847 ///
2848 /// The routes below and above the removed route are notified (see
2849 /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator]
2850 /// has any [Navigator.observers], they will be notified as well (see
2851 /// [NavigatorObserver.didRemove]). The removed route is disposed with its
2852 /// future completed.
2853 ///
2854 /// The given `anchorRoute` must be in the history and must have a route below
2855 /// it; this method will throw an exception if it is not or does not.
2856 ///
2857 /// If non-null, `result` will be used as the result of the route that is
2858 /// removed; the future that had been returned from pushing the removed route
2859 /// will complete with `result`. If provided, must match the type argument of
2860 /// the class of the popped route (`T`).
2861 ///
2862 /// The `T` type argument is the type of the return value of the popped route.
2863 ///
2864 /// The type of `result`, if provided, must match the type argument of the
2865 /// class of the removed route (`T`).
2866 ///
2867 /// Ongoing gestures within the current route are canceled.
2868 /// {@endtemplate}
2869 @optionalTypeArgs
2870 static void removeRouteBelow<T extends Object?>(
2871 BuildContext context,
2872 Route<T> anchorRoute, [
2873 T? result,
2874 ]) {
2875 return Navigator.of(context).removeRouteBelow<T>(anchorRoute, result);
2876 }
2877
2878 /// The state from the closest instance of this class that encloses the given
2879 /// context.
2880 ///
2881 /// Typical usage is as follows:
2882 ///
2883 /// ```dart
2884 /// Navigator.of(context)
2885 /// ..pop()
2886 /// ..pop()
2887 /// ..pushNamed('/settings');
2888 /// ```
2889 ///
2890 /// If `rootNavigator` is set to true, the state from the furthest instance of
2891 /// this class is given instead. Useful for pushing contents above all
2892 /// subsequent instances of [Navigator].
2893 ///
2894 /// If there is no [Navigator] in the given `context`, this function will throw
2895 /// a [FlutterError] in debug mode, and an exception in release mode.
2896 ///
2897 /// This method can be expensive (it walks the element tree).
2898 static NavigatorState of(BuildContext context, {bool rootNavigator = false}) {
2899 NavigatorState? navigator;
2900 if (context case StatefulElement(:final NavigatorState state)) {
2901 navigator = state;
2902 }
2903
2904 navigator = rootNavigator
2905 ? context.findRootAncestorStateOfType<NavigatorState>() ?? navigator
2906 : navigator ?? context.findAncestorStateOfType<NavigatorState>();
2907
2908 assert(() {
2909 if (navigator == null) {
2910 throw FlutterError(
2911 'Navigator operation requested with a context that does not include a Navigator.\n'
2912 'The context used to push or pop routes from the Navigator must be that of a '
2913 'widget that is a descendant of a Navigator widget.',
2914 );
2915 }
2916 return true;
2917 }());
2918 return navigator!;
2919 }
2920
2921 /// The state from the closest instance of this class that encloses the given
2922 /// context, if any.
2923 ///
2924 /// Typical usage is as follows:
2925 ///
2926 /// ```dart
2927 /// NavigatorState? navigatorState = Navigator.maybeOf(context);
2928 /// if (navigatorState != null) {
2929 /// navigatorState
2930 /// ..pop()
2931 /// ..pop()
2932 /// ..pushNamed('/settings');
2933 /// }
2934 /// ```
2935 ///
2936 /// If `rootNavigator` is set to true, the state from the furthest instance of
2937 /// this class is given instead. Useful for pushing contents above all
2938 /// subsequent instances of [Navigator].
2939 ///
2940 /// Will return null if there is no ancestor [Navigator] in the `context`.
2941 ///
2942 /// This method can be expensive (it walks the element tree).
2943 static NavigatorState? maybeOf(BuildContext context, {bool rootNavigator = false}) {
2944 NavigatorState? navigator;
2945 if (context case StatefulElement(:final NavigatorState state)) {
2946 navigator = state;
2947 }
2948
2949 return rootNavigator
2950 ? context.findRootAncestorStateOfType<NavigatorState>() ?? navigator
2951 : navigator ?? context.findAncestorStateOfType<NavigatorState>();
2952 }
2953
2954 /// Turn a route name into a set of [Route] objects.
2955 ///
2956 /// This is the default value of [onGenerateInitialRoutes], which is used if
2957 /// [initialRoute] is not null.
2958 ///
2959 /// If this string starts with a `/` character and has multiple `/` characters
2960 /// in it, then the string is split on those characters and substrings from
2961 /// the start of the string up to each such character are, in turn, used as
2962 /// routes to push.
2963 ///
2964 /// For example, if the route `/stocks/HOOLI` was used as the [initialRoute],
2965 /// then the [Navigator] would push the following routes on startup: `/`,
2966 /// `/stocks`, `/stocks/HOOLI`. This enables deep linking while allowing the
2967 /// application to maintain a predictable route history.
2968 static List<Route<dynamic>> defaultGenerateInitialRoutes(
2969 NavigatorState navigator,
2970 String initialRouteName,
2971 ) {
2972 final List<Route<dynamic>?> result = <Route<dynamic>?>[];
2973 if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
2974 initialRouteName = initialRouteName.substring(1); // strip leading '/'
2975 assert(Navigator.defaultRouteName == '/');
2976 List<String>? debugRouteNames;
2977 assert(() {
2978 debugRouteNames = <String>[Navigator.defaultRouteName];
2979 return true;
2980 }());
2981 result.add(
2982 navigator._routeNamed<dynamic>(
2983 Navigator.defaultRouteName,
2984 arguments: null,
2985 allowNull: true,
2986 ),
2987 );
2988 final List<String> routeParts = initialRouteName.split('/');
2989 if (initialRouteName.isNotEmpty) {
2990 String routeName = '';
2991 for (final String part in routeParts) {
2992 routeName += '/$part';
2993 assert(() {
2994 debugRouteNames!.add(routeName);
2995 return true;
2996 }());
2997 result.add(navigator._routeNamed<dynamic>(routeName, arguments: null, allowNull: true));
2998 }
2999 }
3000 if (result.last == null) {
3001 assert(() {
3002 FlutterError.reportError(
3003 FlutterErrorDetails(
3004 exception:
3005 'Could not navigate to initial route.\n'
3006 'The requested route name was: "/$initialRouteName"\n'
3007 'There was no corresponding route in the app, and therefore the initial route specified will be '
3008 'ignored and "${Navigator.defaultRouteName}" will be used instead.',
3009 ),
3010 );
3011 return true;
3012 }());
3013 for (final Route<dynamic>? route in result) {
3014 route?.dispose();
3015 }
3016 result.clear();
3017 }
3018 } else if (initialRouteName != Navigator.defaultRouteName) {
3019 // If initialRouteName wasn't '/', then we try to get it with allowNull:true, so that if that fails,
3020 // we fall back to '/' (without allowNull:true, see below).
3021 result.add(
3022 navigator._routeNamed<dynamic>(initialRouteName, arguments: null, allowNull: true),
3023 );
3024 }
3025 // Null route might be a result of gap in initialRouteName
3026 //
3027 // For example, routes = ['A', 'A/B/C'], and initialRouteName = 'A/B/C'
3028 // This should result in result = ['A', null, 'A/B/C'] where 'A/B' produces
3029 // the null. In this case, we want to filter out the null and return
3030 // result = ['A', 'A/B/C'].
3031 result.removeWhere((Route<dynamic>? route) => route == null);
3032 if (result.isEmpty) {
3033 result.add(navigator._routeNamed<dynamic>(Navigator.defaultRouteName, arguments: null));
3034 }
3035 return result.cast<Route<dynamic>>();
3036 }
3037
3038 @override
3039 NavigatorState createState() => NavigatorState();
3040}
3041
3042// The _RouteLifecycle state machine (only goes down):
3043//
3044// [creation of a _RouteEntry]
3045// |
3046// +
3047// |\
3048// | \
3049// | staging
3050// | /
3051// |/
3052// +-+----------+--+-------+
3053// / | | |
3054// / | | |
3055// / | | |
3056// / | | |
3057// / | | |
3058// pushReplace push* add* replace*
3059// \ | | |
3060// \ | | /
3061// +--pushing# adding /
3062// \ / /
3063// \ / /
3064// idle--+-----+
3065// / \
3066// / +------+
3067// / | |
3068// / | complete*
3069// | | /
3070// pop* remove*
3071// / \
3072// / removing#
3073// popping# |
3074// | |
3075// [finalizeRoute] |
3076// \ |
3077// dispose*
3078// |
3079// disposing
3080// |
3081// disposed
3082// |
3083// |
3084// [_RouteEntry garbage collected]
3085// (terminal state)
3086//
3087// * These states are transient; as soon as _flushHistoryUpdates is run the
3088// route entry will exit that state.
3089// # These states await futures or other events, then transition automatically.
3090enum _RouteLifecycle {
3091 staging, // we will wait for transition delegate to decide what to do with this route.
3092 //
3093 // routes that are present:
3094 //
3095 add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
3096 adding, // we'll waiting for the future from didPush of top-most route to complete
3097 // routes that are ready for transition.
3098 push, // we'll want to run install, didPush, etc; a route added via push() and friends
3099 pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends
3100 pushing, // we're waiting for the future from didPush to complete
3101 replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends
3102 idle, // route is being harmless
3103 //
3104 // routes that are not present:
3105 //
3106 // routes that should be included in route announcement and should still listen to transition changes.
3107 pop, // we'll want to call didPop
3108 complete, // we'll want to call didComplete,
3109 remove, // we'll want to run didReplace/didRemove etc
3110 // routes should not be included in route announcement but should still listen to transition changes.
3111 popping, // we're waiting for the route to call finalizeRoute to switch to dispose
3112 removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose
3113 // routes that are completely removed from the navigator and overlay.
3114 dispose, // we will dispose the route momentarily
3115 disposing, // The entry is waiting for its widget subtree to be disposed
3116 // first. It is stored in _entryWaitingForSubTreeDisposal while
3117 // awaiting that.
3118 disposed, // we have disposed the route
3119}
3120
3121typedef _RouteEntryPredicate = bool Function(_RouteEntry entry);
3122
3123/// Placeholder for a route.
3124class _RoutePlaceholder {
3125 const _RoutePlaceholder();
3126}
3127
3128class _RouteEntry extends RouteTransitionRecord {
3129 _RouteEntry(
3130 this.route, {
3131 required _RouteLifecycle initialState,
3132 required this.pageBased,
3133 this.restorationInformation,
3134 }) : assert(!pageBased || route.settings is Page),
3135 assert(
3136 initialState == _RouteLifecycle.staging ||
3137 initialState == _RouteLifecycle.add ||
3138 initialState == _RouteLifecycle.push ||
3139 initialState == _RouteLifecycle.pushReplace ||
3140 initialState == _RouteLifecycle.replace,
3141 ),
3142 currentState = initialState {
3143 assert(debugMaybeDispatchCreated('widgets', '_RouteEntry', this));
3144 }
3145
3146 @override
3147 final Route<dynamic> route;
3148 final _RestorationInformation? restorationInformation;
3149 final bool pageBased;
3150
3151 /// The limit this route entry will attempt to pop in the case of route being
3152 /// remove as a result of a page update.
3153 static const int kDebugPopAttemptLimit = 100;
3154
3155 static const _RoutePlaceholder notAnnounced = _RoutePlaceholder();
3156
3157 _RouteLifecycle currentState;
3158 _RoutePlaceholder? lastAnnouncedPreviousRoute =
3159 notAnnounced; // last argument to Route.didChangePrevious
3160 WeakReference<_RoutePlaceholder> lastAnnouncedPoppedNextRoute = WeakReference<_RoutePlaceholder>(
3161 notAnnounced,
3162 ); // last argument to Route.didPopNext
3163 _RoutePlaceholder? lastAnnouncedNextRoute = notAnnounced; // last argument to Route.didChangeNext
3164 int? lastFocusNode; // The last focused semantic node for the route entry.
3165
3166 /// Restoration ID to be used for the encapsulating route when restoration is
3167 /// enabled for it or null if restoration cannot be enabled for it.
3168 String? get restorationId {
3169 // User-provided restoration ids of Pages are prefixed with 'p+'. Generated
3170 // ids for pageless routes are prefixed with 'r+' to avoid clashes.
3171 if (pageBased) {
3172 final Page<Object?> page = route.settings as Page<Object?>;
3173 return page.restorationId != null ? 'p+${page.restorationId}' : null;
3174 }
3175 if (restorationInformation != null) {
3176 return 'r+${restorationInformation!.restorationScopeId}';
3177 }
3178 return null;
3179 }
3180
3181 bool canUpdateFrom(Page<dynamic> page) {
3182 if (!willBePresent) {
3183 return false;
3184 }
3185 if (!pageBased) {
3186 return false;
3187 }
3188 final Page<dynamic> routePage = route.settings as Page<dynamic>;
3189 return page.canUpdate(routePage);
3190 }
3191
3192 void handleAdd({required NavigatorState navigator, required Route<dynamic>? previousPresent}) {
3193 assert(currentState == _RouteLifecycle.add);
3194 assert(navigator._debugLocked);
3195 currentState = _RouteLifecycle.adding;
3196 navigator._observedRouteAdditions.add(_NavigatorPushObservation(route, previousPresent));
3197 }
3198
3199 void handlePush({
3200 required NavigatorState navigator,
3201 required bool isNewFirst,
3202 required Route<dynamic>? previous,
3203 required Route<dynamic>? previousPresent,
3204 }) {
3205 assert(
3206 currentState == _RouteLifecycle.push ||
3207 currentState == _RouteLifecycle.pushReplace ||
3208 currentState == _RouteLifecycle.replace,
3209 );
3210 assert(navigator._debugLocked);
3211 assert(
3212 route._navigator == null,
3213 'The pushed route has already been used. When pushing a route, a new '
3214 'Route object must be provided.',
3215 );
3216 final _RouteLifecycle previousState = currentState;
3217 route._navigator = navigator;
3218 route.install();
3219 assert(route.overlayEntries.isNotEmpty);
3220 if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
3221 final TickerFuture routeFuture = route.didPush();
3222 currentState = _RouteLifecycle.pushing;
3223 routeFuture.whenCompleteOrCancel(() {
3224 if (currentState == _RouteLifecycle.pushing) {
3225 currentState = _RouteLifecycle.idle;
3226 assert(!navigator._debugLocked);
3227 assert(() {
3228 navigator._debugLocked = true;
3229 return true;
3230 }());
3231 navigator._flushHistoryUpdates();
3232 assert(() {
3233 navigator._debugLocked = false;
3234 return true;
3235 }());
3236 }
3237 });
3238 } else {
3239 assert(currentState == _RouteLifecycle.replace);
3240 route.didReplace(previous);
3241 currentState = _RouteLifecycle.idle;
3242 }
3243 if (isNewFirst) {
3244 route.didChangeNext(null);
3245 }
3246
3247 if (previousState == _RouteLifecycle.replace || previousState == _RouteLifecycle.pushReplace) {
3248 navigator._observedRouteAdditions.add(_NavigatorReplaceObservation(route, previousPresent));
3249 } else {
3250 assert(previousState == _RouteLifecycle.push);
3251 navigator._observedRouteAdditions.add(_NavigatorPushObservation(route, previousPresent));
3252 }
3253 }
3254
3255 void handleDidPopNext(Route<dynamic> poppedRoute) {
3256 route.didPopNext(poppedRoute);
3257 lastAnnouncedPoppedNextRoute = WeakReference<Route<dynamic>>(poppedRoute);
3258 if (lastFocusNode != null) {
3259 // Move focus back to the last focused node.
3260 poppedRoute._disposeCompleter.future.then((dynamic result) async {
3261 switch (defaultTargetPlatform) {
3262 case TargetPlatform.android:
3263 // In the Android platform, we have to wait for the system refocus to complete before
3264 // sending the refocus message. Otherwise, the refocus message will be ignored.
3265 // TODO(hangyujin): update this logic if Android provide a better way to do so.
3266 final int? reFocusNode = lastFocusNode;
3267 await Future<void>.delayed(_kAndroidRefocusingDelayDuration);
3268 SystemChannels.accessibility.send(
3269 const FocusSemanticEvent().toMap(nodeId: reFocusNode),
3270 );
3271 case TargetPlatform.iOS:
3272 SystemChannels.accessibility.send(
3273 const FocusSemanticEvent().toMap(nodeId: lastFocusNode),
3274 );
3275 case _:
3276 break;
3277 }
3278 });
3279 }
3280 }
3281
3282 /// Process the to-be-popped route.
3283 ///
3284 /// A route can be marked for pop by transition delegate or Navigator.pop,
3285 /// this method actually pops the route by calling Route.didPop.
3286 ///
3287 /// Returns true if the route is popped; otherwise, returns false if the route
3288 /// refuses to be popped.
3289 bool handlePop({required NavigatorState navigator, required Route<dynamic>? previousPresent}) {
3290 assert(navigator._debugLocked);
3291 assert(route._navigator == navigator);
3292 currentState = _RouteLifecycle.popping;
3293 if (route._popCompleter.isCompleted) {
3294 // This is a page-based route popped through the Navigator.pop. The
3295 // didPop should have been called. No further action is needed.
3296 assert(pageBased);
3297 assert(pendingResult == null);
3298 return true;
3299 }
3300 if (!route.didPop(pendingResult)) {
3301 currentState = _RouteLifecycle.idle;
3302 return false;
3303 }
3304 route.onPopInvokedWithResult(true, pendingResult);
3305 if (pageBased) {
3306 final Page<Object?> page = route.settings as Page<Object?>;
3307 navigator.widget.onDidRemovePage?.call(page);
3308 }
3309 pendingResult = null;
3310 return true;
3311 }
3312
3313 void handleComplete() {
3314 route.didComplete(pendingResult);
3315 pendingResult = null;
3316 assert(route._popCompleter.isCompleted); // implies didComplete was called
3317 currentState = _RouteLifecycle.remove;
3318 }
3319
3320 void handleRemoval({
3321 required NavigatorState navigator,
3322 required Route<dynamic>? previousPresent,
3323 }) {
3324 assert(navigator._debugLocked);
3325 assert(route._navigator == navigator);
3326 currentState = _RouteLifecycle.removing;
3327 if (_reportRemovalToObserver) {
3328 navigator._observedRouteDeletions.add(_NavigatorRemoveObservation(route, previousPresent));
3329 }
3330 }
3331
3332 void didAdd({required NavigatorState navigator, required bool isNewFirst}) {
3333 assert(route._navigator == null);
3334 route._navigator = navigator;
3335 route.install();
3336 assert(route.overlayEntries.isNotEmpty);
3337 route.didAdd();
3338 currentState = _RouteLifecycle.idle;
3339 if (isNewFirst) {
3340 route.didChangeNext(null);
3341 }
3342 }
3343
3344 Object? pendingResult;
3345
3346 void pop<T>(T? result) {
3347 assert(isPresent);
3348 pendingResult = result;
3349 currentState = _RouteLifecycle.pop;
3350 }
3351
3352 bool _reportRemovalToObserver = true;
3353
3354 // Route completes with `result` and is removed.
3355 void complete<T>(T result, {bool isReplaced = false}) {
3356 assert(
3357 !pageBased || isWaitingForExitingDecision,
3358 'A page-based route cannot be completed using imperative api, provide a '
3359 'new list without the corresponding Page to Navigator.pages instead. ',
3360 );
3361 if (currentState.index >= _RouteLifecycle.remove.index) {
3362 return;
3363 }
3364 assert(isPresent);
3365 _reportRemovalToObserver = !isReplaced;
3366 pendingResult = result;
3367 currentState = _RouteLifecycle.complete;
3368 }
3369
3370 void finalize() {
3371 assert(currentState.index < _RouteLifecycle.dispose.index);
3372 currentState = _RouteLifecycle.dispose;
3373 }
3374
3375 /// Disposes this route entry and its [route] immediately.
3376 ///
3377 /// This method does not wait for the widget subtree of the [route] to unmount
3378 /// before disposing.
3379 void forcedDispose() {
3380 assert(currentState.index < _RouteLifecycle.disposed.index);
3381 assert(debugMaybeDispatchDisposed(this));
3382 currentState = _RouteLifecycle.disposed;
3383 route.dispose();
3384 }
3385
3386 /// Disposes this route entry and its [route].
3387 ///
3388 /// This method waits for the widget subtree of the [route] to unmount before
3389 /// disposing. If subtree is already unmounted, this method calls
3390 /// [forcedDispose] immediately.
3391 ///
3392 /// Use [forcedDispose] if the [route] need to be disposed immediately.
3393 void dispose() {
3394 assert(currentState.index < _RouteLifecycle.disposing.index);
3395 currentState = _RouteLifecycle.disposing;
3396
3397 // If the overlay entries are still mounted, widgets in the route's subtree
3398 // may still reference resources from the route and we delay disposal of
3399 // the route until the overlay entries are no longer mounted.
3400 // Since the overlay entry is the root of the route's subtree it will only
3401 // get unmounted after every other widget in the subtree has been unmounted.
3402
3403 final Iterable<OverlayEntry> mountedEntries = route.overlayEntries.where(
3404 (OverlayEntry e) => e.mounted,
3405 );
3406
3407 if (mountedEntries.isEmpty) {
3408 forcedDispose();
3409 return;
3410 }
3411
3412 int mounted = mountedEntries.length;
3413 assert(mounted > 0);
3414 final NavigatorState navigator = route._navigator!;
3415 navigator._entryWaitingForSubTreeDisposal.add(this);
3416 for (final OverlayEntry entry in mountedEntries) {
3417 late VoidCallback listener;
3418 listener = () {
3419 assert(mounted > 0);
3420 assert(!entry.mounted);
3421 mounted--;
3422 entry.removeListener(listener);
3423 if (mounted == 0) {
3424 assert(route.overlayEntries.every((OverlayEntry e) => !e.mounted));
3425 // This is a listener callback of one of the overlayEntries in this
3426 // route. Disposing the route also disposes its overlayEntries and
3427 // violates the rule that a change notifier can't be disposed during
3428 // its notifying callback.
3429 //
3430 // Use a microtask to ensure the overlayEntries have finished
3431 // notifying their listeners before disposing.
3432 return scheduleMicrotask(() {
3433 if (!navigator._entryWaitingForSubTreeDisposal.remove(this)) {
3434 // This route must have been destroyed as a result of navigator
3435 // force dispose.
3436 assert(route._navigator == null && !navigator.mounted);
3437 return;
3438 }
3439 assert(currentState == _RouteLifecycle.disposing);
3440 forcedDispose();
3441 });
3442 }
3443 };
3444 entry.addListener(listener);
3445 }
3446 }
3447
3448 bool get willBePresent {
3449 return currentState.index <= _RouteLifecycle.idle.index &&
3450 currentState.index >= _RouteLifecycle.add.index;
3451 }
3452
3453 bool get isPresent {
3454 return currentState.index <= _RouteLifecycle.remove.index &&
3455 currentState.index >= _RouteLifecycle.add.index;
3456 }
3457
3458 bool get isPresentForRestoration => currentState.index <= _RouteLifecycle.idle.index;
3459
3460 bool get suitableForAnnouncement {
3461 return currentState.index <= _RouteLifecycle.removing.index &&
3462 currentState.index >= _RouteLifecycle.push.index;
3463 }
3464
3465 bool get suitableForTransitionAnimation {
3466 return currentState.index <= _RouteLifecycle.remove.index &&
3467 currentState.index >= _RouteLifecycle.push.index;
3468 }
3469
3470 bool shouldAnnounceChangeToNext(Route<dynamic>? nextRoute) {
3471 assert(nextRoute != lastAnnouncedNextRoute);
3472 // Do not announce if `next` changes from a just popped route to null. We
3473 // already announced this change by calling didPopNext.
3474 return !(nextRoute == null && lastAnnouncedPoppedNextRoute.target == lastAnnouncedNextRoute);
3475 }
3476
3477 static bool isPresentPredicate(_RouteEntry entry) => entry.isPresent;
3478 static bool suitableForTransitionAnimationPredicate(_RouteEntry entry) =>
3479 entry.suitableForTransitionAnimation;
3480 static bool willBePresentPredicate(_RouteEntry entry) => entry.willBePresent;
3481
3482 static _RouteEntryPredicate isRoutePredicate(Route<dynamic> route) {
3483 return (_RouteEntry entry) => entry.route == route;
3484 }
3485
3486 @override
3487 bool get isWaitingForEnteringDecision => currentState == _RouteLifecycle.staging;
3488
3489 @override
3490 bool get isWaitingForExitingDecision => _isWaitingForExitingDecision;
3491 bool _isWaitingForExitingDecision = false;
3492
3493 void markNeedsExitingDecision() => _isWaitingForExitingDecision = true;
3494
3495 @override
3496 void markForPush() {
3497 assert(
3498 isWaitingForEnteringDecision && !isWaitingForExitingDecision,
3499 'This route cannot be marked for push. Either a decision has already been '
3500 'made or it does not require an explicit decision on how to transition in.',
3501 );
3502 currentState = _RouteLifecycle.push;
3503 }
3504
3505 @override
3506 void markForAdd() {
3507 assert(
3508 isWaitingForEnteringDecision && !isWaitingForExitingDecision,
3509 'This route cannot be marked for add. Either a decision has already been '
3510 'made or it does not require an explicit decision on how to transition in.',
3511 );
3512 currentState = _RouteLifecycle.add;
3513 }
3514
3515 @override
3516 void markForPop([dynamic result]) {
3517 assert(
3518 !isWaitingForEnteringDecision && isWaitingForExitingDecision && isPresent,
3519 'This route cannot be marked for pop. Either a decision has already been '
3520 'made or it does not require an explicit decision on how to transition out.',
3521 );
3522 // Remove state that prevents a pop, e.g. LocalHistoryEntry[s].
3523 int attempt = 0;
3524 while (route.willHandlePopInternally) {
3525 assert(() {
3526 attempt += 1;
3527 return attempt < kDebugPopAttemptLimit;
3528 }(), 'Attempted to pop $route $kDebugPopAttemptLimit times, but still failed');
3529 final bool popResult = route.didPop(result);
3530 assert(!popResult);
3531 }
3532 pop<dynamic>(result);
3533 _isWaitingForExitingDecision = false;
3534 }
3535
3536 @override
3537 void markForComplete([dynamic result]) {
3538 assert(
3539 !isWaitingForEnteringDecision && isWaitingForExitingDecision && isPresent,
3540 'This route cannot be marked for complete. Either a decision has already '
3541 'been made or it does not require an explicit decision on how to transition '
3542 'out.',
3543 );
3544 complete<dynamic>(result);
3545 _isWaitingForExitingDecision = false;
3546 }
3547
3548 bool get restorationEnabled => route.restorationScopeId.value != null;
3549 set restorationEnabled(bool value) {
3550 assert(!value || restorationId != null);
3551 route._updateRestorationId(value ? restorationId : null);
3552 }
3553}
3554
3555abstract class _NavigatorObservation {
3556 _NavigatorObservation(this.primaryRoute, this.secondaryRoute);
3557 final Route<dynamic> primaryRoute;
3558 final Route<dynamic>? secondaryRoute;
3559
3560 void notify(NavigatorObserver observer);
3561}
3562
3563class _NavigatorPushObservation extends _NavigatorObservation {
3564 _NavigatorPushObservation(super.primaryRoute, super.secondaryRoute);
3565
3566 @override
3567 void notify(NavigatorObserver observer) {
3568 observer.didPush(primaryRoute, secondaryRoute);
3569 }
3570}
3571
3572class _NavigatorPopObservation extends _NavigatorObservation {
3573 _NavigatorPopObservation(super.primaryRoute, super.secondaryRoute);
3574
3575 @override
3576 void notify(NavigatorObserver observer) {
3577 observer.didPop(primaryRoute, secondaryRoute);
3578 }
3579}
3580
3581class _NavigatorRemoveObservation extends _NavigatorObservation {
3582 _NavigatorRemoveObservation(super.primaryRoute, super.secondaryRoute);
3583
3584 @override
3585 void notify(NavigatorObserver observer) {
3586 observer.didRemove(primaryRoute, secondaryRoute);
3587 }
3588}
3589
3590class _NavigatorReplaceObservation extends _NavigatorObservation {
3591 _NavigatorReplaceObservation(super.primaryRoute, super.secondaryRoute);
3592
3593 @override
3594 void notify(NavigatorObserver observer) {
3595 observer.didReplace(newRoute: primaryRoute, oldRoute: secondaryRoute);
3596 }
3597}
3598
3599typedef _IndexWhereCallback = bool Function(_RouteEntry element);
3600
3601/// A collection of _RouteEntries representing a navigation history.
3602///
3603/// Acts as a ChangeNotifier and notifies after its List of _RouteEntries is
3604/// mutated.
3605class _History extends Iterable<_RouteEntry> with ChangeNotifier {
3606 /// Creates an instance of [_History].
3607 _History() {
3608 if (kFlutterMemoryAllocationsEnabled) {
3609 ChangeNotifier.maybeDispatchObjectCreation(this);
3610 }
3611 }
3612
3613 final List<_RouteEntry> _value = <_RouteEntry>[];
3614
3615 int indexWhere(_IndexWhereCallback test, [int start = 0]) {
3616 return _value.indexWhere(test, start);
3617 }
3618
3619 void add(_RouteEntry element) {
3620 _value.add(element);
3621 notifyListeners();
3622 }
3623
3624 void addAll(Iterable<_RouteEntry> elements) {
3625 _value.addAll(elements);
3626 if (elements.isNotEmpty) {
3627 notifyListeners();
3628 }
3629 }
3630
3631 void clear() {
3632 final bool valueWasEmpty = _value.isEmpty;
3633 _value.clear();
3634 if (!valueWasEmpty) {
3635 notifyListeners();
3636 }
3637 }
3638
3639 void insert(int index, _RouteEntry element) {
3640 _value.insert(index, element);
3641 notifyListeners();
3642 }
3643
3644 _RouteEntry removeAt(int index) {
3645 final _RouteEntry entry = _value.removeAt(index);
3646 notifyListeners();
3647 return entry;
3648 }
3649
3650 _RouteEntry removeLast() {
3651 final _RouteEntry entry = _value.removeLast();
3652 notifyListeners();
3653 return entry;
3654 }
3655
3656 _RouteEntry operator [](int index) {
3657 return _value[index];
3658 }
3659
3660 @override
3661 Iterator<_RouteEntry> get iterator {
3662 return _value.iterator;
3663 }
3664
3665 @override
3666 String toString() {
3667 return _value.toString();
3668 }
3669}
3670
3671/// The state for a [Navigator] widget.
3672///
3673/// A reference to this class can be obtained by calling [Navigator.of].
3674class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
3675 late GlobalKey<OverlayState> _overlayKey;
3676 final _History _history = _History();
3677
3678 /// A set for entries that are waiting to dispose until their subtrees are
3679 /// disposed.
3680 ///
3681 /// These entries are not considered to be in the _history and will usually
3682 /// remove themselves from this set once they can dispose.
3683 ///
3684 /// The navigator keep track of these entries so that, in case the navigator
3685 /// itself is disposed, it can dispose these entries immediately.
3686 final Set<_RouteEntry> _entryWaitingForSubTreeDisposal = <_RouteEntry>{};
3687 final _HistoryProperty _serializableHistory = _HistoryProperty();
3688 final Queue<_NavigatorObservation> _observedRouteAdditions = Queue<_NavigatorObservation>();
3689 final Queue<_NavigatorObservation> _observedRouteDeletions = Queue<_NavigatorObservation>();
3690
3691 /// The [FocusNode] for the [Focus] that encloses the routes.
3692 final FocusNode focusNode = FocusNode(debugLabel: 'Navigator');
3693
3694 bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
3695
3696 HeroController? _heroControllerFromScope;
3697
3698 late List<NavigatorObserver> _effectiveObservers;
3699
3700 bool get _usingPagesAPI => widget.pages != const <Page<dynamic>>[];
3701
3702 void _handleHistoryChanged() {
3703 final bool navigatorCanPop = canPop();
3704 final bool routeBlocksPop;
3705 if (!navigatorCanPop) {
3706 final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
3707 routeBlocksPop =
3708 lastEntry != null && lastEntry.route.popDisposition == RoutePopDisposition.doNotPop;
3709 } else {
3710 routeBlocksPop = false;
3711 }
3712 final NavigationNotification notification = NavigationNotification(
3713 canHandlePop: navigatorCanPop || routeBlocksPop,
3714 );
3715 // Avoid dispatching a notification in the middle of a build.
3716 switch (SchedulerBinding.instance.schedulerPhase) {
3717 case SchedulerPhase.postFrameCallbacks:
3718 notification.dispatch(context);
3719 case SchedulerPhase.idle:
3720 case SchedulerPhase.midFrameMicrotasks:
3721 case SchedulerPhase.persistentCallbacks:
3722 case SchedulerPhase.transientCallbacks:
3723 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
3724 if (!mounted) {
3725 return;
3726 }
3727 notification.dispatch(context);
3728 }, debugLabel: 'Navigator.dispatchNotification');
3729 }
3730 }
3731
3732 bool _debugCheckPageApiParameters() {
3733 if (!_usingPagesAPI) {
3734 return true;
3735 }
3736 if (widget.pages.isEmpty) {
3737 FlutterError.reportError(
3738 FlutterErrorDetails(
3739 exception: FlutterError(
3740 'The Navigator.pages must not be empty to use the '
3741 'Navigator.pages API',
3742 ),
3743 library: 'widget library',
3744 stack: StackTrace.current,
3745 ),
3746 );
3747 } else if ((widget.onDidRemovePage == null) == (widget.onPopPage == null)) {
3748 FlutterError.reportError(
3749 FlutterErrorDetails(
3750 exception: FlutterError(
3751 'Either onDidRemovePage or onPopPage must be provided to use the '
3752 'Navigator.pages API but not both.',
3753 ),
3754 library: 'widget library',
3755 stack: StackTrace.current,
3756 ),
3757 );
3758 }
3759 return true;
3760 }
3761
3762 @protected
3763 @override
3764 void initState() {
3765 super.initState();
3766 assert(_debugCheckPageApiParameters());
3767 for (final NavigatorObserver observer in widget.observers) {
3768 assert(observer.navigator == null);
3769 NavigatorObserver._navigators[observer] = this;
3770 }
3771 _effectiveObservers = widget.observers;
3772
3773 // We have to manually extract the inherited widget in initState because
3774 // the current context is not fully initialized.
3775 final HeroControllerScope? heroControllerScope =
3776 context.getElementForInheritedWidgetOfExactType<HeroControllerScope>()?.widget
3777 as HeroControllerScope?;
3778 _updateHeroController(heroControllerScope?.controller);
3779
3780 if (widget.reportsRouteUpdateToEngine) {
3781 SystemNavigator.selectSingleEntryHistory();
3782 }
3783
3784 ServicesBinding.instance.accessibilityFocus.addListener(_recordLastFocus);
3785 _history.addListener(_handleHistoryChanged);
3786 }
3787
3788 // Record the last focused node in route entry.
3789 void _recordLastFocus() {
3790 final _RouteEntry? entry = _history.where(_RouteEntry.isPresentPredicate).lastOrNull;
3791 entry?.lastFocusNode = ServicesBinding.instance.accessibilityFocus.value;
3792 }
3793
3794 // Use [_nextPagelessRestorationScopeId] to get the next id.
3795 final RestorableNum<int> _rawNextPagelessRestorationScopeId = RestorableNum<int>(0);
3796
3797 int get _nextPagelessRestorationScopeId => _rawNextPagelessRestorationScopeId.value++;
3798
3799 @protected
3800 @override
3801 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
3802 registerForRestoration(_rawNextPagelessRestorationScopeId, 'id');
3803 registerForRestoration(_serializableHistory, 'history');
3804
3805 // Delete everything in the old history and clear the overlay.
3806 _forcedDisposeAllRouteEntries();
3807 assert(_history.isEmpty);
3808 _overlayKey = GlobalKey<OverlayState>();
3809
3810 // Populate the new history from restoration data.
3811 _history.addAll(_serializableHistory.restoreEntriesForPage(null, this));
3812 for (final Page<dynamic> page in widget.pages) {
3813 final _RouteEntry entry = _RouteEntry(
3814 page.createRoute(context),
3815 pageBased: true,
3816 initialState: _RouteLifecycle.add,
3817 );
3818 assert(
3819 entry.route.settings == page,
3820 'The settings getter of a page-based Route must return a Page object. '
3821 'Please set the settings to the Page in the Page.createRoute method.',
3822 );
3823 _history.add(entry);
3824 _history.addAll(_serializableHistory.restoreEntriesForPage(entry, this));
3825 }
3826
3827 // If there was nothing to restore, we need to process the initial route.
3828 if (!_serializableHistory.hasData) {
3829 String? initialRoute = widget.initialRoute;
3830 if (widget.pages.isEmpty) {
3831 initialRoute ??= Navigator.defaultRouteName;
3832 }
3833 if (initialRoute != null) {
3834 _history.addAll(
3835 widget
3836 .onGenerateInitialRoutes(this, widget.initialRoute ?? Navigator.defaultRouteName)
3837 .map(
3838 (Route<dynamic> route) => _RouteEntry(
3839 route,
3840 pageBased: false,
3841 initialState: _RouteLifecycle.add,
3842 restorationInformation: route.settings.name != null
3843 ? _RestorationInformation.named(
3844 name: route.settings.name!,
3845 arguments: null,
3846 restorationScopeId: _nextPagelessRestorationScopeId,
3847 )
3848 : null,
3849 ),
3850 ),
3851 );
3852 }
3853 }
3854
3855 assert(
3856 _history.isNotEmpty,
3857 'All routes returned by onGenerateInitialRoutes are not restorable. '
3858 'Please make sure that all routes returned by onGenerateInitialRoutes '
3859 'have their RouteSettings defined with names that are defined in the '
3860 "app's routes table.",
3861 );
3862 assert(!_debugLocked);
3863 assert(() {
3864 _debugLocked = true;
3865 return true;
3866 }());
3867 _flushHistoryUpdates();
3868 assert(() {
3869 _debugLocked = false;
3870 return true;
3871 }());
3872 }
3873
3874 @protected
3875 @override
3876 void didToggleBucket(RestorationBucket? oldBucket) {
3877 super.didToggleBucket(oldBucket);
3878 if (bucket != null) {
3879 _serializableHistory.update(_history);
3880 } else {
3881 _serializableHistory.clear();
3882 }
3883 }
3884
3885 @override
3886 String? get restorationId => widget.restorationScopeId;
3887
3888 @protected
3889 @override
3890 void didChangeDependencies() {
3891 super.didChangeDependencies();
3892 _updateHeroController(HeroControllerScope.maybeOf(context));
3893 for (final _RouteEntry entry in _history) {
3894 if (entry.route.navigator == this) {
3895 entry.route.changedExternalState();
3896 }
3897 }
3898 }
3899
3900 /// Dispose all lingering router entries immediately.
3901 void _forcedDisposeAllRouteEntries() {
3902 _entryWaitingForSubTreeDisposal.removeWhere((_RouteEntry entry) {
3903 entry.forcedDispose();
3904 return true;
3905 });
3906 while (_history.isNotEmpty) {
3907 _disposeRouteEntry(_history.removeLast(), graceful: false);
3908 }
3909 }
3910
3911 static void _disposeRouteEntry(_RouteEntry entry, {required bool graceful}) {
3912 for (final OverlayEntry overlayEntry in entry.route.overlayEntries) {
3913 overlayEntry.remove();
3914 }
3915 if (graceful) {
3916 entry.dispose();
3917 } else {
3918 entry.forcedDispose();
3919 }
3920 }
3921
3922 void _updateHeroController(HeroController? newHeroController) {
3923 if (_heroControllerFromScope != newHeroController) {
3924 if (newHeroController != null) {
3925 // Makes sure the same hero controller is not shared between two navigators.
3926 assert(() {
3927 // It is possible that the hero controller subscribes to an existing
3928 // navigator. We are fine as long as that navigator gives up the hero
3929 // controller at the end of the build.
3930 if (newHeroController.navigator != null) {
3931 final NavigatorState previousOwner = newHeroController.navigator!;
3932 ServicesBinding.instance.addPostFrameCallback((Duration timestamp) {
3933 // We only check if this navigator still owns the hero controller.
3934 if (_heroControllerFromScope == newHeroController) {
3935 final bool hasHeroControllerOwnerShip = _heroControllerFromScope!.navigator == this;
3936 if (!hasHeroControllerOwnerShip ||
3937 previousOwner._heroControllerFromScope == newHeroController) {
3938 final NavigatorState otherOwner = hasHeroControllerOwnerShip
3939 ? previousOwner
3940 : _heroControllerFromScope!.navigator!;
3941 FlutterError.reportError(
3942 FlutterErrorDetails(
3943 exception: FlutterError(
3944 'A HeroController can not be shared by multiple Navigators. '
3945 'The Navigators that share the same HeroController are:\n'
3946 '- $this\n'
3947 '- $otherOwner\n'
3948 'Please create a HeroControllerScope for each Navigator or '
3949 'use a HeroControllerScope.none to prevent subtree from '
3950 'receiving a HeroController.',
3951 ),
3952 library: 'widget library',
3953 stack: StackTrace.current,
3954 ),
3955 );
3956 }
3957 }
3958 }, debugLabel: 'Navigator.checkHeroControllerOwnership');
3959 }
3960 return true;
3961 }());
3962 NavigatorObserver._navigators[newHeroController] = this;
3963 }
3964 // Only unsubscribe the hero controller when it is currently subscribe to
3965 // this navigator.
3966 if (_heroControllerFromScope?.navigator == this) {
3967 NavigatorObserver._navigators[_heroControllerFromScope!] = null;
3968 }
3969 _heroControllerFromScope = newHeroController;
3970 _updateEffectiveObservers();
3971 }
3972 }
3973
3974 void _updateEffectiveObservers() {
3975 if (_heroControllerFromScope != null) {
3976 _effectiveObservers = widget.observers + <NavigatorObserver>[_heroControllerFromScope!];
3977 } else {
3978 _effectiveObservers = widget.observers;
3979 }
3980 }
3981
3982 @protected
3983 @override
3984 void didUpdateWidget(Navigator oldWidget) {
3985 super.didUpdateWidget(oldWidget);
3986 assert(_debugCheckPageApiParameters());
3987 if (oldWidget.observers != widget.observers) {
3988 for (final NavigatorObserver observer in oldWidget.observers) {
3989 NavigatorObserver._navigators[observer] = null;
3990 }
3991 for (final NavigatorObserver observer in widget.observers) {
3992 assert(observer.navigator == null);
3993 NavigatorObserver._navigators[observer] = this;
3994 }
3995 _updateEffectiveObservers();
3996 }
3997 if (oldWidget.pages != widget.pages && !restorePending) {
3998 assert(() {
3999 if (widget.pages.isEmpty) {
4000 FlutterError.reportError(
4001 FlutterErrorDetails(
4002 exception: FlutterError(
4003 'The Navigator.pages must not be empty to use the '
4004 'Navigator.pages API',
4005 ),
4006 library: 'widget library',
4007 stack: StackTrace.current,
4008 ),
4009 );
4010 }
4011 return true;
4012 }());
4013 _updatePages();
4014 }
4015
4016 for (final _RouteEntry entry in _history) {
4017 if (entry.route.navigator == this) {
4018 entry.route.changedExternalState();
4019 }
4020 }
4021 }
4022
4023 void _debugCheckDuplicatedPageKeys() {
4024 assert(() {
4025 final Set<Key> keyReservation = <Key>{};
4026 for (final Page<dynamic> page in widget.pages) {
4027 final LocalKey? key = page.key;
4028 if (key != null) {
4029 assert(!keyReservation.contains(key));
4030 keyReservation.add(key);
4031 }
4032 }
4033 return true;
4034 }());
4035 }
4036
4037 @protected
4038 @override
4039 void deactivate() {
4040 for (final NavigatorObserver observer in _effectiveObservers) {
4041 NavigatorObserver._navigators[observer] = null;
4042 }
4043 _effectiveObservers = <NavigatorObserver>[];
4044 super.deactivate();
4045 }
4046
4047 @protected
4048 @override
4049 void activate() {
4050 super.activate();
4051 _updateEffectiveObservers();
4052 for (final NavigatorObserver observer in _effectiveObservers) {
4053 assert(observer.navigator == null);
4054 NavigatorObserver._navigators[observer] = this;
4055 }
4056 }
4057
4058 @protected
4059 @override
4060 void dispose() {
4061 assert(!_debugLocked);
4062 assert(() {
4063 _debugLocked = true;
4064 return true;
4065 }());
4066 assert(_effectiveObservers.isEmpty);
4067 _updateHeroController(null);
4068 focusNode.dispose();
4069 _forcedDisposeAllRouteEntries();
4070 _rawNextPagelessRestorationScopeId.dispose();
4071 _serializableHistory.dispose();
4072 userGestureInProgressNotifier.dispose();
4073 ServicesBinding.instance.accessibilityFocus.removeListener(_recordLastFocus);
4074 _history.removeListener(_handleHistoryChanged);
4075 _history.dispose();
4076 super.dispose();
4077 // don't unlock, so that the object becomes unusable
4078 assert(_debugLocked);
4079 }
4080
4081 /// The overlay this navigator uses for its visual presentation.
4082 OverlayState? get overlay => _overlayKey.currentState;
4083
4084 Iterable<OverlayEntry> get _allRouteOverlayEntries {
4085 return <OverlayEntry>[for (final _RouteEntry entry in _history) ...entry.route.overlayEntries];
4086 }
4087
4088 _RouteEntry? _lastTopmostRoute;
4089 String? _lastAnnouncedRouteName;
4090
4091 bool _debugUpdatingPage = false;
4092 void _updatePages() {
4093 assert(() {
4094 assert(!_debugUpdatingPage);
4095 _debugCheckDuplicatedPageKeys();
4096 _debugUpdatingPage = true;
4097 return true;
4098 }());
4099
4100 // This attempts to diff the new pages list (widget.pages) with
4101 // the old _RouteEntry(s) list (_history), and produces a new list of
4102 // _RouteEntry(s) to be the new list of _history. This method roughly
4103 // follows the same outline of RenderObjectElement.updateChildren.
4104 //
4105 // The cases it tries to optimize for are:
4106 // - the old list is empty
4107 // - All the pages in the new list can match the page-based routes in the old
4108 // list, and their orders are the same.
4109 // - there is an insertion or removal of one or more page-based route in
4110 // only one place in the list
4111 // If a page-based route with a key is in both lists, it will be synced.
4112 // Page-based routes without keys might be synced but there is no guarantee.
4113
4114 // The general approach is to sync the entire new list backwards, as follows:
4115 // 1. Walk the lists from the bottom, syncing nodes, and record pageless routes,
4116 // until you no longer have matching nodes.
4117 // 2. Walk the lists from the top, without syncing nodes, until you no
4118 // longer have matching nodes. We'll sync these nodes at the end. We
4119 // don't sync them now because we want to sync all the nodes in order
4120 // from beginning to end.
4121 // At this point we narrowed the old and new lists to the point
4122 // where the nodes no longer match.
4123 // 3. Walk the narrowed part of the old list to get the list of
4124 // keys.
4125 // 4. Walk the narrowed part of the new list forwards:
4126 // * Create a new _RouteEntry for non-keyed items and record them for
4127 // transitionDelegate.
4128 // * Sync keyed items with the source if it exists.
4129 // 5. Walk the narrowed part of the old list again to records the
4130 // _RouteEntry(s), as well as pageless routes, needed to be removed for
4131 // transitionDelegate.
4132 // 5. Walk the top of the list again, syncing the nodes and recording
4133 // pageless routes.
4134 // 6. Use transitionDelegate for explicit decisions on how _RouteEntry(s)
4135 // transition in or off the screens.
4136 // 7. Fill pageless routes back into the new history.
4137
4138 bool needsExplicitDecision = false;
4139 int newPagesBottom = 0;
4140 int oldEntriesBottom = 0;
4141 int newPagesTop = widget.pages.length - 1;
4142 int oldEntriesTop = _history.length - 1;
4143
4144 final List<_RouteEntry> newHistory = <_RouteEntry>[];
4145 final Map<_RouteEntry?, List<_RouteEntry>> pageRouteToPagelessRoutes =
4146 <_RouteEntry?, List<_RouteEntry>>{};
4147
4148 // Updates the bottom of the list.
4149 _RouteEntry? previousOldPageRouteEntry;
4150 while (oldEntriesBottom <= oldEntriesTop) {
4151 final _RouteEntry oldEntry = _history[oldEntriesBottom];
4152 assert(oldEntry.currentState != _RouteLifecycle.disposed);
4153 // Records pageless route. The bottom most pageless routes will be
4154 // stored in key = null.
4155 if (!oldEntry.pageBased) {
4156 final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent(
4157 previousOldPageRouteEntry,
4158 () => <_RouteEntry>[],
4159 );
4160 pagelessRoutes.add(oldEntry);
4161 oldEntriesBottom += 1;
4162 continue;
4163 }
4164 if (newPagesBottom > newPagesTop) {
4165 break;
4166 }
4167 final Page<dynamic> newPage = widget.pages[newPagesBottom];
4168 if (!oldEntry.canUpdateFrom(newPage)) {
4169 break;
4170 }
4171 previousOldPageRouteEntry = oldEntry;
4172 oldEntry.route._updateSettings(newPage);
4173 newHistory.add(oldEntry);
4174 newPagesBottom += 1;
4175 oldEntriesBottom += 1;
4176 }
4177
4178 final List<_RouteEntry> unattachedPagelessRoutes = <_RouteEntry>[];
4179 // Scans the top of the list until we found a page-based route that cannot be
4180 // updated.
4181 while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
4182 final _RouteEntry oldEntry = _history[oldEntriesTop];
4183 assert(oldEntry.currentState != _RouteLifecycle.disposed);
4184 if (!oldEntry.pageBased) {
4185 unattachedPagelessRoutes.add(oldEntry);
4186 oldEntriesTop -= 1;
4187 continue;
4188 }
4189 final Page<dynamic> newPage = widget.pages[newPagesTop];
4190 if (!oldEntry.canUpdateFrom(newPage)) {
4191 break;
4192 }
4193
4194 // We found the page for all the consecutive pageless routes below. Attach these
4195 // pageless routes to the page.
4196 if (unattachedPagelessRoutes.isNotEmpty) {
4197 pageRouteToPagelessRoutes.putIfAbsent(
4198 oldEntry,
4199 () => List<_RouteEntry>.of(unattachedPagelessRoutes),
4200 );
4201 unattachedPagelessRoutes.clear();
4202 }
4203
4204 oldEntriesTop -= 1;
4205 newPagesTop -= 1;
4206 }
4207 // Reverts the pageless routes that cannot be updated.
4208 oldEntriesTop += unattachedPagelessRoutes.length;
4209
4210 // Scans middle of the old entries and records the page key to old entry map.
4211 int oldEntriesBottomToScan = oldEntriesBottom;
4212 final Map<LocalKey, _RouteEntry> pageKeyToOldEntry = <LocalKey, _RouteEntry>{};
4213 // This set contains entries that are transitioning out but are still in
4214 // the route stack.
4215 final Set<_RouteEntry> phantomEntries = <_RouteEntry>{};
4216 while (oldEntriesBottomToScan <= oldEntriesTop) {
4217 final _RouteEntry oldEntry = _history[oldEntriesBottomToScan];
4218 oldEntriesBottomToScan += 1;
4219 assert(oldEntry.currentState != _RouteLifecycle.disposed);
4220 // Pageless routes will be recorded when we update the middle of the old
4221 // list.
4222 if (!oldEntry.pageBased) {
4223 continue;
4224 }
4225
4226 final Page<dynamic> page = oldEntry.route.settings as Page<dynamic>;
4227 if (page.key == null) {
4228 continue;
4229 }
4230
4231 if (!oldEntry.willBePresent) {
4232 phantomEntries.add(oldEntry);
4233 continue;
4234 }
4235 assert(!pageKeyToOldEntry.containsKey(page.key));
4236 pageKeyToOldEntry[page.key!] = oldEntry;
4237 }
4238
4239 // Updates the middle of the list.
4240 while (newPagesBottom <= newPagesTop) {
4241 final Page<dynamic> nextPage = widget.pages[newPagesBottom];
4242 newPagesBottom += 1;
4243 if (nextPage.key == null ||
4244 !pageKeyToOldEntry.containsKey(nextPage.key) ||
4245 !pageKeyToOldEntry[nextPage.key]!.canUpdateFrom(nextPage)) {
4246 // There is no matching key in the old history, we need to create a new
4247 // route and wait for the transition delegate to decide how to add
4248 // it into the history.
4249 final _RouteEntry newEntry = _RouteEntry(
4250 nextPage.createRoute(context),
4251 pageBased: true,
4252 initialState: _RouteLifecycle.staging,
4253 );
4254 needsExplicitDecision = true;
4255 assert(
4256 newEntry.route.settings == nextPage,
4257 'The settings getter of a page-based Route must return a Page object. '
4258 'Please set the settings to the Page in the Page.createRoute method.',
4259 );
4260 newHistory.add(newEntry);
4261 } else {
4262 // Removes the key from pageKeyToOldEntry to indicate it is taken.
4263 final _RouteEntry matchingEntry = pageKeyToOldEntry.remove(nextPage.key)!;
4264 assert(matchingEntry.canUpdateFrom(nextPage));
4265 matchingEntry.route._updateSettings(nextPage);
4266 newHistory.add(matchingEntry);
4267 }
4268 }
4269
4270 // Any remaining old routes that do not have a match will need to be removed.
4271 final Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute =
4272 <RouteTransitionRecord?, RouteTransitionRecord>{};
4273 while (oldEntriesBottom <= oldEntriesTop) {
4274 final _RouteEntry potentialEntryToRemove = _history[oldEntriesBottom];
4275 oldEntriesBottom += 1;
4276
4277 if (!potentialEntryToRemove.pageBased) {
4278 assert(previousOldPageRouteEntry != null);
4279 final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent(
4280 previousOldPageRouteEntry,
4281 () => <_RouteEntry>[],
4282 );
4283 pagelessRoutes.add(potentialEntryToRemove);
4284 if (previousOldPageRouteEntry!.isWaitingForExitingDecision &&
4285 potentialEntryToRemove.willBePresent) {
4286 potentialEntryToRemove.markNeedsExitingDecision();
4287 }
4288 continue;
4289 }
4290
4291 final Page<dynamic> potentialPageToRemove =
4292 potentialEntryToRemove.route.settings as Page<dynamic>;
4293 // Marks for transition delegate to remove if this old page does not have
4294 // a key, was not taken during updating the middle of new page, or is
4295 // already transitioning out.
4296 if (potentialPageToRemove.key == null ||
4297 pageKeyToOldEntry.containsKey(potentialPageToRemove.key) ||
4298 phantomEntries.contains(potentialEntryToRemove)) {
4299 locationToExitingPageRoute[previousOldPageRouteEntry] = potentialEntryToRemove;
4300 // We only need a decision if it has not already been popped.
4301 if (potentialEntryToRemove.willBePresent) {
4302 potentialEntryToRemove.markNeedsExitingDecision();
4303 }
4304 }
4305 previousOldPageRouteEntry = potentialEntryToRemove;
4306 }
4307
4308 // We've scanned the whole list.
4309 assert(oldEntriesBottom == oldEntriesTop + 1);
4310 assert(newPagesBottom == newPagesTop + 1);
4311 newPagesTop = widget.pages.length - 1;
4312 oldEntriesTop = _history.length - 1;
4313 // Verifies we either reach the bottom or the oldEntriesBottom must be updatable
4314 // by newPagesBottom.
4315 assert(() {
4316 if (oldEntriesBottom <= oldEntriesTop) {
4317 return newPagesBottom <= newPagesTop &&
4318 _history[oldEntriesBottom].pageBased &&
4319 _history[oldEntriesBottom].canUpdateFrom(widget.pages[newPagesBottom]);
4320 } else {
4321 return newPagesBottom > newPagesTop;
4322 }
4323 }());
4324
4325 // Updates the top of the list.
4326 while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
4327 final _RouteEntry oldEntry = _history[oldEntriesBottom];
4328 assert(oldEntry.currentState != _RouteLifecycle.disposed);
4329 if (!oldEntry.pageBased) {
4330 assert(previousOldPageRouteEntry != null);
4331 final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent(
4332 previousOldPageRouteEntry,
4333 () => <_RouteEntry>[],
4334 );
4335 pagelessRoutes.add(oldEntry);
4336 continue;
4337 }
4338 previousOldPageRouteEntry = oldEntry;
4339 final Page<dynamic> newPage = widget.pages[newPagesBottom];
4340 assert(oldEntry.canUpdateFrom(newPage));
4341 oldEntry.route._updateSettings(newPage);
4342 newHistory.add(oldEntry);
4343 oldEntriesBottom += 1;
4344 newPagesBottom += 1;
4345 }
4346
4347 // Finally, uses transition delegate to make explicit decision if needed.
4348 needsExplicitDecision = needsExplicitDecision || locationToExitingPageRoute.isNotEmpty;
4349 Iterable<_RouteEntry> results = newHistory;
4350 if (needsExplicitDecision) {
4351 results = widget.transitionDelegate
4352 ._transition(
4353 newPageRouteHistory: newHistory,
4354 locationToExitingPageRoute: locationToExitingPageRoute,
4355 pageRouteToPagelessRoutes: pageRouteToPagelessRoutes,
4356 )
4357 .cast<_RouteEntry>();
4358 }
4359 _history.clear();
4360 // Adds the leading pageless routes if there is any.
4361 if (pageRouteToPagelessRoutes.containsKey(null)) {
4362 _history.addAll(pageRouteToPagelessRoutes[null]!);
4363 }
4364 for (final _RouteEntry result in results) {
4365 _history.add(result);
4366 if (pageRouteToPagelessRoutes.containsKey(result)) {
4367 _history.addAll(pageRouteToPagelessRoutes[result]!);
4368 }
4369 }
4370 assert(() {
4371 _debugUpdatingPage = false;
4372 return true;
4373 }());
4374 assert(() {
4375 _debugLocked = true;
4376 return true;
4377 }());
4378 _flushHistoryUpdates();
4379 assert(() {
4380 _debugLocked = false;
4381 return true;
4382 }());
4383 }
4384
4385 bool _flushingHistory = false;
4386
4387 void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
4388 assert(_debugLocked && !_debugUpdatingPage);
4389 _flushingHistory = true;
4390 // Clean up the list, sending updates to the routes that changed. Notably,
4391 // we don't send the didChangePrevious/didChangeNext updates to those that
4392 // did not change at this point, because we're not yet sure exactly what the
4393 // routes will be at the end of the day (some might get disposed).
4394 int index = _history.length - 1;
4395 _RouteEntry? next;
4396 _RouteEntry? entry = _history[index];
4397 _RouteEntry? previous = index > 0 ? _history[index - 1] : null;
4398 bool canRemoveOrAdd =
4399 false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
4400 Route<dynamic>?
4401 poppedRoute; // The route that should trigger didPopNext on the top active route.
4402 bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
4403 final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
4404 while (index >= 0) {
4405 switch (entry!.currentState) {
4406 case _RouteLifecycle.add:
4407 assert(rearrangeOverlay);
4408 entry.handleAdd(
4409 navigator: this,
4410 previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
4411 );
4412 assert(entry.currentState == _RouteLifecycle.adding);
4413 continue;
4414 case _RouteLifecycle.adding:
4415 if (canRemoveOrAdd || next == null) {
4416 entry.didAdd(navigator: this, isNewFirst: next == null);
4417 assert(entry.currentState == _RouteLifecycle.idle);
4418 continue;
4419 }
4420 case _RouteLifecycle.push:
4421 case _RouteLifecycle.pushReplace:
4422 case _RouteLifecycle.replace:
4423 assert(rearrangeOverlay);
4424 entry.handlePush(
4425 navigator: this,
4426 previous: previous?.route,
4427 previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
4428 isNewFirst: next == null,
4429 );
4430 assert(entry.currentState != _RouteLifecycle.push);
4431 assert(entry.currentState != _RouteLifecycle.pushReplace);
4432 assert(entry.currentState != _RouteLifecycle.replace);
4433 if (entry.currentState == _RouteLifecycle.idle) {
4434 continue;
4435 }
4436 case _RouteLifecycle.pushing: // Will exit this state when animation completes.
4437 if (!seenTopActiveRoute && poppedRoute != null) {
4438 entry.handleDidPopNext(poppedRoute);
4439 }
4440 seenTopActiveRoute = true;
4441 case _RouteLifecycle.idle:
4442 if (!seenTopActiveRoute && poppedRoute != null) {
4443 entry.handleDidPopNext(poppedRoute);
4444 }
4445 seenTopActiveRoute = true;
4446 // This route is idle, so we are allowed to remove subsequent (earlier)
4447 // routes that are waiting to be removed silently:
4448 canRemoveOrAdd = true;
4449 case _RouteLifecycle.pop:
4450 if (!entry.handlePop(
4451 navigator: this,
4452 previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
4453 )) {
4454 assert(entry.currentState == _RouteLifecycle.idle);
4455 continue;
4456 }
4457 if (!seenTopActiveRoute) {
4458 if (poppedRoute != null) {
4459 entry.handleDidPopNext(poppedRoute);
4460 }
4461 poppedRoute = entry.route;
4462 }
4463 _observedRouteDeletions.add(
4464 _NavigatorPopObservation(
4465 entry.route,
4466 _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
4467 ),
4468 );
4469 if (entry.currentState == _RouteLifecycle.dispose) {
4470 // The pop finished synchronously. This can happen if transition
4471 // duration is zero.
4472 continue;
4473 }
4474 assert(entry.currentState == _RouteLifecycle.popping);
4475 canRemoveOrAdd = true;
4476 case _RouteLifecycle.popping:
4477 // Will exit this state when animation completes.
4478 break;
4479 case _RouteLifecycle.complete:
4480 entry.handleComplete();
4481 assert(entry.currentState == _RouteLifecycle.remove);
4482 continue;
4483 case _RouteLifecycle.remove:
4484 if (!seenTopActiveRoute) {
4485 if (poppedRoute != null) {
4486 entry.route.didPopNext(poppedRoute);
4487 }
4488 poppedRoute = null;
4489 }
4490 entry.handleRemoval(
4491 navigator: this,
4492 previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
4493 );
4494 assert(entry.currentState == _RouteLifecycle.removing);
4495 continue;
4496 case _RouteLifecycle.removing:
4497 if (!canRemoveOrAdd && next != null) {
4498 // We aren't allowed to remove this route yet.
4499 break;
4500 }
4501 if (entry.pageBased) {
4502 widget.onDidRemovePage?.call(entry.route.settings as Page<Object?>);
4503 }
4504 entry.currentState = _RouteLifecycle.dispose;
4505 continue;
4506 case _RouteLifecycle.dispose:
4507 // Delay disposal until didChangeNext/didChangePrevious have been sent.
4508 toBeDisposed.add(_history.removeAt(index));
4509 entry = next;
4510 case _RouteLifecycle.disposing:
4511 case _RouteLifecycle.disposed:
4512 case _RouteLifecycle.staging:
4513 assert(false);
4514 }
4515 index -= 1;
4516 next = entry;
4517 entry = previous;
4518 previous = index > 0 ? _history[index - 1] : null;
4519 }
4520 // Informs navigator observers about route changes.
4521 _flushObserverNotifications();
4522
4523 // Now that the list is clean, send the didChangeNext/didChangePrevious
4524 // notifications.
4525 _flushRouteAnnouncement();
4526
4527 final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
4528 if (lastEntry != null && _lastTopmostRoute != lastEntry) {
4529 for (final NavigatorObserver observer in _effectiveObservers) {
4530 observer.didChangeTop(lastEntry.route, _lastTopmostRoute?.route);
4531 }
4532 }
4533 _lastTopmostRoute = lastEntry;
4534 // Announce route name changes.
4535 if (widget.reportsRouteUpdateToEngine) {
4536 final String? routeName = lastEntry?.route.settings.name;
4537 if (routeName != null && routeName != _lastAnnouncedRouteName) {
4538 SystemNavigator.routeInformationUpdated(uri: Uri.parse(routeName));
4539 _lastAnnouncedRouteName = routeName;
4540 }
4541 }
4542
4543 // Lastly, removes the overlay entries of all marked entries and disposes
4544 // them.
4545 for (final _RouteEntry entry in toBeDisposed) {
4546 _disposeRouteEntry(entry, graceful: true);
4547 }
4548 if (rearrangeOverlay) {
4549 overlay?.rearrange(_allRouteOverlayEntries);
4550 }
4551 if (bucket != null) {
4552 _serializableHistory.update(_history);
4553 }
4554 _flushingHistory = false;
4555 }
4556
4557 void _flushObserverNotifications() {
4558 if (_effectiveObservers.isEmpty) {
4559 _observedRouteDeletions.clear();
4560 _observedRouteAdditions.clear();
4561 return;
4562 }
4563 while (_observedRouteAdditions.isNotEmpty) {
4564 final _NavigatorObservation observation = _observedRouteAdditions.removeLast();
4565 _effectiveObservers.forEach(observation.notify);
4566 }
4567
4568 while (_observedRouteDeletions.isNotEmpty) {
4569 final _NavigatorObservation observation = _observedRouteDeletions.removeFirst();
4570 _effectiveObservers.forEach(observation.notify);
4571 }
4572 }
4573
4574 void _flushRouteAnnouncement() {
4575 int index = _history.length - 1;
4576 while (index >= 0) {
4577 final _RouteEntry entry = _history[index];
4578 if (!entry.suitableForAnnouncement) {
4579 index -= 1;
4580 continue;
4581 }
4582 final _RouteEntry? next = _getRouteAfter(
4583 index + 1,
4584 _RouteEntry.suitableForTransitionAnimationPredicate,
4585 );
4586
4587 if (next?.route != entry.lastAnnouncedNextRoute) {
4588 if (entry.shouldAnnounceChangeToNext(next?.route)) {
4589 entry.route.didChangeNext(next?.route);
4590 }
4591 entry.lastAnnouncedNextRoute = next?.route;
4592 }
4593 final _RouteEntry? previous = _getRouteBefore(
4594 index - 1,
4595 _RouteEntry.suitableForTransitionAnimationPredicate,
4596 );
4597 if (previous?.route != entry.lastAnnouncedPreviousRoute) {
4598 entry.route.didChangePrevious(previous?.route);
4599 entry.lastAnnouncedPreviousRoute = previous?.route;
4600 }
4601 index -= 1;
4602 }
4603 }
4604
4605 _RouteEntry? _getRouteBefore(int index, _RouteEntryPredicate predicate) {
4606 index = _getIndexBefore(index, predicate);
4607 return index >= 0 ? _history[index] : null;
4608 }
4609
4610 int _getIndexBefore(int index, _RouteEntryPredicate predicate) {
4611 while (index >= 0 && !predicate(_history[index])) {
4612 index -= 1;
4613 }
4614 return index;
4615 }
4616
4617 _RouteEntry? _getRouteAfter(int index, _RouteEntryPredicate predicate) {
4618 while (index < _history.length && !predicate(_history[index])) {
4619 index += 1;
4620 }
4621 return index < _history.length ? _history[index] : null;
4622 }
4623
4624 Route<T?>? _routeNamed<T>(String name, {required Object? arguments, bool allowNull = false}) {
4625 assert(!_debugLocked);
4626 if (allowNull && widget.onGenerateRoute == null) {
4627 return null;
4628 }
4629 assert(() {
4630 if (widget.onGenerateRoute == null) {
4631 throw FlutterError(
4632 'Navigator.onGenerateRoute was null, but the route named "$name" was referenced.\n'
4633 'To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or '
4634 'pushNamedAndRemoveUntil), the Navigator must be provided with an '
4635 'onGenerateRoute handler.\n'
4636 'The Navigator was:\n'
4637 ' $this',
4638 );
4639 }
4640 return true;
4641 }());
4642 final RouteSettings settings = RouteSettings(name: name, arguments: arguments);
4643 Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
4644 if (route == null && !allowNull) {
4645 assert(() {
4646 if (widget.onUnknownRoute == null) {
4647 throw FlutterError.fromParts(<DiagnosticsNode>[
4648 ErrorSummary(
4649 'Navigator.onGenerateRoute returned null when requested to build route "$name".',
4650 ),
4651 ErrorDescription(
4652 'The onGenerateRoute callback must never return null, unless an onUnknownRoute '
4653 'callback is provided as well.',
4654 ),
4655 DiagnosticsProperty<NavigatorState>(
4656 'The Navigator was',
4657 this,
4658 style: DiagnosticsTreeStyle.errorProperty,
4659 ),
4660 ]);
4661 }
4662 return true;
4663 }());
4664 route = widget.onUnknownRoute!(settings) as Route<T?>?;
4665 assert(() {
4666 if (route == null) {
4667 throw FlutterError.fromParts(<DiagnosticsNode>[
4668 ErrorSummary(
4669 'Navigator.onUnknownRoute returned null when requested to build route "$name".',
4670 ),
4671 ErrorDescription('The onUnknownRoute callback must never return null.'),
4672 DiagnosticsProperty<NavigatorState>(
4673 'The Navigator was',
4674 this,
4675 style: DiagnosticsTreeStyle.errorProperty,
4676 ),
4677 ]);
4678 }
4679 return true;
4680 }());
4681 }
4682 assert(route != null || allowNull);
4683 return route;
4684 }
4685
4686 /// Push a named route onto the navigator.
4687 ///
4688 /// {@macro flutter.widgets.navigator.pushNamed}
4689 ///
4690 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
4691 ///
4692 /// {@macro flutter.widgets.Navigator.pushNamed}
4693 ///
4694 /// {@tool snippet}
4695 ///
4696 /// Typical usage is as follows:
4697 ///
4698 /// ```dart
4699 /// void _aaronBurrSir() {
4700 /// navigator.pushNamed('/nyc/1776');
4701 /// }
4702 /// ```
4703 /// {@end-tool}
4704 ///
4705 /// See also:
4706 ///
4707 /// * [restorablePushNamed], which pushes a route that can be restored
4708 /// during state restoration.
4709 @optionalTypeArgs
4710 Future<T?> pushNamed<T extends Object?>(String routeName, {Object? arguments}) {
4711 return push<T?>(_routeNamed<T>(routeName, arguments: arguments)!);
4712 }
4713
4714 /// Push a named route onto the navigator.
4715 ///
4716 /// {@macro flutter.widgets.navigator.restorablePushNamed}
4717 ///
4718 /// {@macro flutter.widgets.navigator.pushNamed}
4719 ///
4720 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
4721 ///
4722 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
4723 ///
4724 /// {@tool snippet}
4725 ///
4726 /// Typical usage is as follows:
4727 ///
4728 /// ```dart
4729 /// void _openDetails() {
4730 /// navigator.restorablePushNamed('/nyc/1776');
4731 /// }
4732 /// ```
4733 /// {@end-tool}
4734 @optionalTypeArgs
4735 String restorablePushNamed<T extends Object?>(String routeName, {Object? arguments}) {
4736 assert(
4737 debugIsSerializableForRestoration(arguments),
4738 'The arguments object must be serializable via the StandardMessageCodec.',
4739 );
4740 final _RouteEntry entry = _RestorationInformation.named(
4741 name: routeName,
4742 arguments: arguments,
4743 restorationScopeId: _nextPagelessRestorationScopeId,
4744 ).toRouteEntry(this, initialState: _RouteLifecycle.push);
4745 _pushEntry(entry);
4746 return entry.restorationId!;
4747 }
4748
4749 /// Replace the current route of the navigator by pushing the route named
4750 /// [routeName] and then disposing the previous route once the new route has
4751 /// finished animating in.
4752 ///
4753 /// {@macro flutter.widgets.navigator.pushReplacementNamed}
4754 ///
4755 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
4756 ///
4757 /// {@macro flutter.widgets.Navigator.pushNamed}
4758 ///
4759 /// {@tool snippet}
4760 ///
4761 /// Typical usage is as follows:
4762 ///
4763 /// ```dart
4764 /// void _startBike() {
4765 /// navigator.pushReplacementNamed('/jouett/1781');
4766 /// }
4767 /// ```
4768 /// {@end-tool}
4769 ///
4770 /// See also:
4771 ///
4772 /// * [restorablePushReplacementNamed], which pushes a replacement route that
4773 /// can be restored during state restoration.
4774 @optionalTypeArgs
4775 Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
4776 String routeName, {
4777 TO? result,
4778 Object? arguments,
4779 }) {
4780 return pushReplacement<T?, TO>(
4781 _routeNamed<T>(routeName, arguments: arguments)!,
4782 result: result,
4783 );
4784 }
4785
4786 /// Replace the current route of the navigator by pushing the route named
4787 /// [routeName] and then disposing the previous route once the new route has
4788 /// finished animating in.
4789 ///
4790 /// {@macro flutter.widgets.navigator.restorablePushReplacementNamed}
4791 ///
4792 /// {@macro flutter.widgets.navigator.pushReplacementNamed}
4793 ///
4794 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
4795 ///
4796 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
4797 ///
4798 /// {@tool snippet}
4799 ///
4800 /// Typical usage is as follows:
4801 ///
4802 /// ```dart
4803 /// void _startCar() {
4804 /// navigator.restorablePushReplacementNamed('/jouett/1781');
4805 /// }
4806 /// ```
4807 /// {@end-tool}
4808 @optionalTypeArgs
4809 String restorablePushReplacementNamed<T extends Object?, TO extends Object?>(
4810 String routeName, {
4811 TO? result,
4812 Object? arguments,
4813 }) {
4814 assert(
4815 debugIsSerializableForRestoration(arguments),
4816 'The arguments object must be serializable via the StandardMessageCodec.',
4817 );
4818 final _RouteEntry entry = _RestorationInformation.named(
4819 name: routeName,
4820 arguments: arguments,
4821 restorationScopeId: _nextPagelessRestorationScopeId,
4822 ).toRouteEntry(this, initialState: _RouteLifecycle.pushReplace);
4823 _pushReplacementEntry(entry, result);
4824 return entry.restorationId!;
4825 }
4826
4827 /// Pop the current route off the navigator and push a named route in its
4828 /// place.
4829 ///
4830 /// {@macro flutter.widgets.navigator.popAndPushNamed}
4831 ///
4832 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
4833 ///
4834 /// {@macro flutter.widgets.Navigator.pushNamed}
4835 ///
4836 /// {@tool snippet}
4837 ///
4838 /// Typical usage is as follows:
4839 ///
4840 /// ```dart
4841 /// void _begin() {
4842 /// navigator.popAndPushNamed('/nyc/1776');
4843 /// }
4844 /// ```
4845 /// {@end-tool}
4846 ///
4847 /// See also:
4848 ///
4849 /// * [restorablePopAndPushNamed], which pushes a new route that can be
4850 /// restored during state restoration.
4851 @optionalTypeArgs
4852 Future<T?> popAndPushNamed<T extends Object?, TO extends Object?>(
4853 String routeName, {
4854 TO? result,
4855 Object? arguments,
4856 }) {
4857 pop<TO>(result);
4858 return pushNamed<T>(routeName, arguments: arguments);
4859 }
4860
4861 /// Pop the current route off the navigator and push a named route in its
4862 /// place.
4863 ///
4864 /// {@macro flutter.widgets.navigator.restorablePopAndPushNamed}
4865 ///
4866 /// {@macro flutter.widgets.navigator.popAndPushNamed}
4867 ///
4868 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
4869 ///
4870 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
4871 ///
4872 /// {@tool snippet}
4873 ///
4874 /// Typical usage is as follows:
4875 ///
4876 /// ```dart
4877 /// void _end() {
4878 /// navigator.restorablePopAndPushNamed('/nyc/1776');
4879 /// }
4880 /// ```
4881 /// {@end-tool}
4882 @optionalTypeArgs
4883 String restorablePopAndPushNamed<T extends Object?, TO extends Object?>(
4884 String routeName, {
4885 TO? result,
4886 Object? arguments,
4887 }) {
4888 pop<TO>(result);
4889 return restorablePushNamed(routeName, arguments: arguments);
4890 }
4891
4892 /// Push the route with the given name onto the navigator, and then remove all
4893 /// the previous routes until the `predicate` returns true.
4894 ///
4895 /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil}
4896 ///
4897 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
4898 ///
4899 /// {@macro flutter.widgets.Navigator.pushNamed}
4900 ///
4901 /// {@tool snippet}
4902 ///
4903 /// Typical usage is as follows:
4904 ///
4905 /// ```dart
4906 /// void _handleOpenCalendar() {
4907 /// navigator.pushNamedAndRemoveUntil('/calendar', ModalRoute.withName('/'));
4908 /// }
4909 /// ```
4910 /// {@end-tool}
4911 ///
4912 /// See also:
4913 ///
4914 /// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can
4915 /// be restored during state restoration.
4916 @optionalTypeArgs
4917 Future<T?> pushNamedAndRemoveUntil<T extends Object?>(
4918 String newRouteName,
4919 RoutePredicate predicate, {
4920 Object? arguments,
4921 }) {
4922 return pushAndRemoveUntil<T?>(_routeNamed<T>(newRouteName, arguments: arguments)!, predicate);
4923 }
4924
4925 /// Push the route with the given name onto the navigator, and then remove all
4926 /// the previous routes until the `predicate` returns true.
4927 ///
4928 /// {@macro flutter.widgets.navigator.restorablePushNamedAndRemoveUntil}
4929 ///
4930 /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil}
4931 ///
4932 /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments}
4933 ///
4934 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
4935 ///
4936 /// {@tool snippet}
4937 ///
4938 /// Typical usage is as follows:
4939 ///
4940 /// ```dart
4941 /// void _openCalendar() {
4942 /// navigator.restorablePushNamedAndRemoveUntil('/calendar', ModalRoute.withName('/'));
4943 /// }
4944 /// ```
4945 /// {@end-tool}
4946 @optionalTypeArgs
4947 String restorablePushNamedAndRemoveUntil<T extends Object?>(
4948 String newRouteName,
4949 RoutePredicate predicate, {
4950 Object? arguments,
4951 }) {
4952 assert(
4953 debugIsSerializableForRestoration(arguments),
4954 'The arguments object must be serializable via the StandardMessageCodec.',
4955 );
4956 final _RouteEntry entry = _RestorationInformation.named(
4957 name: newRouteName,
4958 arguments: arguments,
4959 restorationScopeId: _nextPagelessRestorationScopeId,
4960 ).toRouteEntry(this, initialState: _RouteLifecycle.push);
4961 _pushEntryAndRemoveUntil(entry, predicate);
4962 return entry.restorationId!;
4963 }
4964
4965 /// Push the given route onto the navigator.
4966 ///
4967 /// {@macro flutter.widgets.navigator.push}
4968 ///
4969 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
4970 ///
4971 /// {@tool snippet}
4972 ///
4973 /// Typical usage is as follows:
4974 ///
4975 /// ```dart
4976 /// void _openPage() {
4977 /// navigator.push<void>(
4978 /// MaterialPageRoute<void>(
4979 /// builder: (BuildContext context) => const MyPage(),
4980 /// ),
4981 /// );
4982 /// }
4983 /// ```
4984 /// {@end-tool}
4985 ///
4986 /// See also:
4987 ///
4988 /// * [restorablePush], which pushes a route that can be restored during
4989 /// state restoration.
4990 @optionalTypeArgs
4991 Future<T?> push<T extends Object?>(Route<T> route) {
4992 _pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
4993 return route.popped;
4994 }
4995
4996 bool _debugIsStaticCallback(Function callback) {
4997 bool result = false;
4998 assert(() {
4999 // TODO(goderbauer): remove the kIsWeb check when https://github.com/flutter/flutter/issues/33615 is resolved.
5000 result = kIsWeb || ui.PluginUtilities.getCallbackHandle(callback) != null;
5001 return true;
5002 }());
5003 return result;
5004 }
5005
5006 /// Push a new route onto the navigator.
5007 ///
5008 /// {@macro flutter.widgets.navigator.restorablePush}
5009 ///
5010 /// {@macro flutter.widgets.navigator.push}
5011 ///
5012 /// {@macro flutter.widgets.Navigator.restorablePush}
5013 ///
5014 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
5015 ///
5016 /// {@tool dartpad}
5017 /// Typical usage is as follows:
5018 ///
5019 /// ** See code in examples/api/lib/widgets/navigator/navigator_state.restorable_push.0.dart **
5020 /// {@end-tool}
5021 @optionalTypeArgs
5022 String restorablePush<T extends Object?>(
5023 RestorableRouteBuilder<T> routeBuilder, {
5024 Object? arguments,
5025 }) {
5026 assert(
5027 _debugIsStaticCallback(routeBuilder),
5028 'The provided routeBuilder must be a static function.',
5029 );
5030 assert(
5031 debugIsSerializableForRestoration(arguments),
5032 'The arguments object must be serializable via the StandardMessageCodec.',
5033 );
5034 final _RouteEntry entry = _RestorationInformation.anonymous(
5035 routeBuilder: routeBuilder,
5036 arguments: arguments,
5037 restorationScopeId: _nextPagelessRestorationScopeId,
5038 ).toRouteEntry(this, initialState: _RouteLifecycle.push);
5039 _pushEntry(entry);
5040 return entry.restorationId!;
5041 }
5042
5043 void _pushEntry(_RouteEntry entry) {
5044 assert(!_debugLocked);
5045 assert(() {
5046 _debugLocked = true;
5047 return true;
5048 }());
5049 assert(entry.route._navigator == null);
5050 assert(entry.currentState == _RouteLifecycle.push);
5051 _history.add(entry);
5052 _flushHistoryUpdates();
5053 assert(() {
5054 _debugLocked = false;
5055 return true;
5056 }());
5057 _afterNavigation(entry.route);
5058 }
5059
5060 void _afterNavigation(Route<dynamic>? route) {
5061 if (!kReleaseMode) {
5062 // Among other uses, performance tools use this event to ensure that perf
5063 // stats reflect the time interval since the last navigation event
5064 // occurred, ensuring that stats only reflect the current page.
5065
5066 Map<String, dynamic>? routeJsonable;
5067 if (route != null) {
5068 routeJsonable = <String, dynamic>{};
5069
5070 final String description;
5071 if (route is TransitionRoute<dynamic>) {
5072 final TransitionRoute<dynamic> transitionRoute = route;
5073 description = transitionRoute.debugLabel;
5074 } else {
5075 description = '$route';
5076 }
5077 routeJsonable['description'] = description;
5078
5079 final RouteSettings settings = route.settings;
5080 final Map<String, dynamic> settingsJsonable = <String, dynamic>{'name': settings.name};
5081 if (settings.arguments != null) {
5082 settingsJsonable['arguments'] = jsonEncode(
5083 settings.arguments,
5084 toEncodable: (Object? object) => '$object',
5085 );
5086 }
5087 routeJsonable['settings'] = settingsJsonable;
5088 }
5089
5090 developer.postEvent('Flutter.Navigation', <String, dynamic>{'route': routeJsonable});
5091 }
5092 _cancelActivePointers();
5093 }
5094
5095 /// Replace the current route of the navigator by pushing the given route and
5096 /// then disposing the previous route once the new route has finished
5097 /// animating in.
5098 ///
5099 /// {@macro flutter.widgets.navigator.pushReplacement}
5100 ///
5101 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
5102 ///
5103 /// {@tool snippet}
5104 ///
5105 /// Typical usage is as follows:
5106 ///
5107 /// ```dart
5108 /// void _doOpenPage() {
5109 /// navigator.pushReplacement<void, void>(
5110 /// MaterialPageRoute<void>(
5111 /// builder: (BuildContext context) => const MyHomePage(),
5112 /// ),
5113 /// );
5114 /// }
5115 /// ```
5116 /// {@end-tool}
5117 ///
5118 /// See also:
5119 ///
5120 /// * [restorablePushReplacement], which pushes a replacement route that can
5121 /// be restored during state restoration.
5122 @optionalTypeArgs
5123 Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
5124 Route<T> newRoute, {
5125 TO? result,
5126 }) {
5127 assert(newRoute._navigator == null);
5128 _pushReplacementEntry(
5129 _RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.pushReplace),
5130 result,
5131 );
5132 return newRoute.popped;
5133 }
5134
5135 /// Replace the current route of the navigator by pushing a new route and
5136 /// then disposing the previous route once the new route has finished
5137 /// animating in.
5138 ///
5139 /// {@macro flutter.widgets.navigator.restorablePushReplacement}
5140 ///
5141 /// {@macro flutter.widgets.navigator.pushReplacement}
5142 ///
5143 /// {@macro flutter.widgets.Navigator.restorablePush}
5144 ///
5145 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
5146 ///
5147 /// {@tool dartpad}
5148 /// Typical usage is as follows:
5149 ///
5150 /// ** See code in examples/api/lib/widgets/navigator/navigator_state.restorable_push_replacement.0.dart **
5151 /// {@end-tool}
5152 @optionalTypeArgs
5153 String restorablePushReplacement<T extends Object?, TO extends Object?>(
5154 RestorableRouteBuilder<T> routeBuilder, {
5155 TO? result,
5156 Object? arguments,
5157 }) {
5158 assert(
5159 _debugIsStaticCallback(routeBuilder),
5160 'The provided routeBuilder must be a static function.',
5161 );
5162 assert(
5163 debugIsSerializableForRestoration(arguments),
5164 'The arguments object must be serializable via the StandardMessageCodec.',
5165 );
5166 final _RouteEntry entry = _RestorationInformation.anonymous(
5167 routeBuilder: routeBuilder,
5168 arguments: arguments,
5169 restorationScopeId: _nextPagelessRestorationScopeId,
5170 ).toRouteEntry(this, initialState: _RouteLifecycle.pushReplace);
5171 _pushReplacementEntry(entry, result);
5172 return entry.restorationId!;
5173 }
5174
5175 void _pushReplacementEntry<TO extends Object?>(_RouteEntry entry, TO? result) {
5176 assert(!_debugLocked);
5177 assert(() {
5178 _debugLocked = true;
5179 return true;
5180 }());
5181 assert(entry.route._navigator == null);
5182 assert(_history.isNotEmpty);
5183 assert(
5184 _history.any(_RouteEntry.isPresentPredicate),
5185 'Navigator has no active routes to replace.',
5186 );
5187 assert(entry.currentState == _RouteLifecycle.pushReplace);
5188 _history.lastWhere(_RouteEntry.isPresentPredicate).complete(result, isReplaced: true);
5189 _history.add(entry);
5190 _flushHistoryUpdates();
5191 assert(() {
5192 _debugLocked = false;
5193 return true;
5194 }());
5195 _afterNavigation(entry.route);
5196 }
5197
5198 /// Push the given route onto the navigator, and then remove all the previous
5199 /// routes until the `predicate` returns true.
5200 ///
5201 /// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
5202 ///
5203 /// {@macro flutter.widgets.navigator.pushNamed.returnValue}
5204 ///
5205 /// {@tool snippet}
5206 ///
5207 /// Typical usage is as follows:
5208 ///
5209 /// ```dart
5210 /// void _resetAndOpenPage() {
5211 /// navigator.pushAndRemoveUntil<void>(
5212 /// MaterialPageRoute<void>(builder: (BuildContext context) => const MyHomePage()),
5213 /// ModalRoute.withName('/'),
5214 /// );
5215 /// }
5216 /// ```
5217 /// {@end-tool}
5218 ///
5219 ///
5220 /// See also:
5221 ///
5222 /// * [restorablePushAndRemoveUntil], which pushes a route that can be
5223 /// restored during state restoration.
5224 @optionalTypeArgs
5225 Future<T?> pushAndRemoveUntil<T extends Object?>(Route<T> newRoute, RoutePredicate predicate) {
5226 assert(newRoute._navigator == null);
5227 assert(newRoute.overlayEntries.isEmpty);
5228 _pushEntryAndRemoveUntil(
5229 _RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.push),
5230 predicate,
5231 );
5232 return newRoute.popped;
5233 }
5234
5235 /// Push a new route onto the navigator, and then remove all the previous
5236 /// routes until the `predicate` returns true.
5237 ///
5238 /// {@macro flutter.widgets.navigator.restorablePushAndRemoveUntil}
5239 ///
5240 /// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
5241 ///
5242 /// {@macro flutter.widgets.Navigator.restorablePush}
5243 ///
5244 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
5245 ///
5246 /// {@tool dartpad}
5247 /// Typical usage is as follows:
5248 ///
5249 /// ** See code in examples/api/lib/widgets/navigator/navigator_state.restorable_push_and_remove_until.0.dart **
5250 /// {@end-tool}
5251 @optionalTypeArgs
5252 String restorablePushAndRemoveUntil<T extends Object?>(
5253 RestorableRouteBuilder<T> newRouteBuilder,
5254 RoutePredicate predicate, {
5255 Object? arguments,
5256 }) {
5257 assert(
5258 _debugIsStaticCallback(newRouteBuilder),
5259 'The provided routeBuilder must be a static function.',
5260 );
5261 assert(
5262 debugIsSerializableForRestoration(arguments),
5263 'The arguments object must be serializable via the StandardMessageCodec.',
5264 );
5265 final _RouteEntry entry = _RestorationInformation.anonymous(
5266 routeBuilder: newRouteBuilder,
5267 arguments: arguments,
5268 restorationScopeId: _nextPagelessRestorationScopeId,
5269 ).toRouteEntry(this, initialState: _RouteLifecycle.push);
5270 _pushEntryAndRemoveUntil(entry, predicate);
5271 return entry.restorationId!;
5272 }
5273
5274 void _pushEntryAndRemoveUntil(_RouteEntry entry, RoutePredicate predicate) {
5275 assert(!_debugLocked);
5276 assert(() {
5277 _debugLocked = true;
5278 return true;
5279 }());
5280 assert(entry.route._navigator == null);
5281 assert(entry.route.overlayEntries.isEmpty);
5282 assert(entry.currentState == _RouteLifecycle.push);
5283 int index = _history.length - 1;
5284 _history.add(entry);
5285 while (index >= 0 && !predicate(_history[index].route)) {
5286 if (_history[index].isPresent) {
5287 _history[index].complete(null);
5288 }
5289 index -= 1;
5290 }
5291 _flushHistoryUpdates();
5292
5293 assert(() {
5294 _debugLocked = false;
5295 return true;
5296 }());
5297 _afterNavigation(entry.route);
5298 }
5299
5300 /// Replaces a route on the navigator with a new route.
5301 ///
5302 /// {@macro flutter.widgets.navigator.replace}
5303 ///
5304 /// See also:
5305 ///
5306 /// * [replaceRouteBelow], which is the same but identifies the route to be
5307 /// removed by reference to the route above it, rather than directly.
5308 /// * [restorableReplace], which adds a replacement route that can be
5309 /// restored during state restoration.
5310 @optionalTypeArgs
5311 void replace<T extends Object?>({required Route<dynamic> oldRoute, required Route<T> newRoute}) {
5312 assert(!_debugLocked);
5313 assert(oldRoute._navigator == this);
5314 _replaceEntry(
5315 _RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace),
5316 oldRoute,
5317 );
5318 }
5319
5320 /// Replaces a route on the navigator with a new route.
5321 ///
5322 /// {@macro flutter.widgets.navigator.restorableReplace}
5323 ///
5324 /// {@macro flutter.widgets.navigator.replace}
5325 ///
5326 /// {@macro flutter.widgets.Navigator.restorablePush}
5327 ///
5328 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
5329 @optionalTypeArgs
5330 String restorableReplace<T extends Object?>({
5331 required Route<dynamic> oldRoute,
5332 required RestorableRouteBuilder<T> newRouteBuilder,
5333 Object? arguments,
5334 }) {
5335 assert(oldRoute._navigator == this);
5336 assert(
5337 _debugIsStaticCallback(newRouteBuilder),
5338 'The provided routeBuilder must be a static function.',
5339 );
5340 assert(
5341 debugIsSerializableForRestoration(arguments),
5342 'The arguments object must be serializable via the StandardMessageCodec.',
5343 );
5344 final _RouteEntry entry = _RestorationInformation.anonymous(
5345 routeBuilder: newRouteBuilder,
5346 arguments: arguments,
5347 restorationScopeId: _nextPagelessRestorationScopeId,
5348 ).toRouteEntry(this, initialState: _RouteLifecycle.replace);
5349 _replaceEntry(entry, oldRoute);
5350 return entry.restorationId!;
5351 }
5352
5353 void _replaceEntry(_RouteEntry entry, Route<dynamic> oldRoute) {
5354 assert(!_debugLocked);
5355 if (oldRoute == entry.route) {
5356 return;
5357 }
5358 assert(() {
5359 _debugLocked = true;
5360 return true;
5361 }());
5362 assert(entry.currentState == _RouteLifecycle.replace);
5363 assert(entry.route._navigator == null);
5364 final int index = _history.indexWhere(_RouteEntry.isRoutePredicate(oldRoute));
5365 assert(index >= 0, 'This Navigator does not contain the specified oldRoute.');
5366 assert(
5367 _history[index].isPresent,
5368 'The specified oldRoute has already been removed from the Navigator.',
5369 );
5370 final bool wasCurrent = oldRoute.isCurrent;
5371 _history.insert(index + 1, entry);
5372 _history[index].complete(null, isReplaced: true);
5373 _flushHistoryUpdates();
5374 assert(() {
5375 _debugLocked = false;
5376 return true;
5377 }());
5378 if (wasCurrent) {
5379 _afterNavigation(entry.route);
5380 }
5381 }
5382
5383 /// Replaces a route on the navigator with a new route. The route to be
5384 /// replaced is the one below the given `anchorRoute`.
5385 ///
5386 /// {@macro flutter.widgets.navigator.replaceRouteBelow}
5387 ///
5388 /// See also:
5389 ///
5390 /// * [replace], which is the same but identifies the route to be removed
5391 /// directly.
5392 /// * [restorableReplaceRouteBelow], which adds a replacement route that can
5393 /// be restored during state restoration.
5394 @optionalTypeArgs
5395 void replaceRouteBelow<T extends Object?>({
5396 required Route<dynamic> anchorRoute,
5397 required Route<T> newRoute,
5398 }) {
5399 assert(newRoute._navigator == null);
5400 assert(anchorRoute._navigator == this);
5401 _replaceEntryBelow(
5402 _RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace),
5403 anchorRoute,
5404 );
5405 }
5406
5407 /// Replaces a route on the navigator with a new route. The route to be
5408 /// replaced is the one below the given `anchorRoute`.
5409 ///
5410 /// {@macro flutter.widgets.navigator.restorableReplaceRouteBelow}
5411 ///
5412 /// {@macro flutter.widgets.navigator.replaceRouteBelow}
5413 ///
5414 /// {@macro flutter.widgets.Navigator.restorablePush}
5415 ///
5416 /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue}
5417 @optionalTypeArgs
5418 String restorableReplaceRouteBelow<T extends Object?>({
5419 required Route<dynamic> anchorRoute,
5420 required RestorableRouteBuilder<T> newRouteBuilder,
5421 Object? arguments,
5422 }) {
5423 assert(anchorRoute._navigator == this);
5424 assert(
5425 _debugIsStaticCallback(newRouteBuilder),
5426 'The provided routeBuilder must be a static function.',
5427 );
5428 assert(
5429 debugIsSerializableForRestoration(arguments),
5430 'The arguments object must be serializable via the StandardMessageCodec.',
5431 );
5432 final _RouteEntry entry = _RestorationInformation.anonymous(
5433 routeBuilder: newRouteBuilder,
5434 arguments: arguments,
5435 restorationScopeId: _nextPagelessRestorationScopeId,
5436 ).toRouteEntry(this, initialState: _RouteLifecycle.replace);
5437 _replaceEntryBelow(entry, anchorRoute);
5438 return entry.restorationId!;
5439 }
5440
5441 void _replaceEntryBelow(_RouteEntry entry, Route<dynamic> anchorRoute) {
5442 assert(!_debugLocked);
5443 assert(() {
5444 _debugLocked = true;
5445 return true;
5446 }());
5447 final int anchorIndex = _history.indexWhere(_RouteEntry.isRoutePredicate(anchorRoute));
5448 assert(anchorIndex >= 0, 'This Navigator does not contain the specified anchorRoute.');
5449 assert(
5450 _history[anchorIndex].isPresent,
5451 'The specified anchorRoute has already been removed from the Navigator.',
5452 );
5453 int index = anchorIndex - 1;
5454 while (index >= 0) {
5455 if (_history[index].isPresent) {
5456 break;
5457 }
5458 index -= 1;
5459 }
5460 assert(index >= 0, 'There are no routes below the specified anchorRoute.');
5461 _history.insert(index + 1, entry);
5462 _history[index].complete(null, isReplaced: true);
5463 _flushHistoryUpdates();
5464 assert(() {
5465 _debugLocked = false;
5466 return true;
5467 }());
5468 }
5469
5470 /// Whether the navigator can be popped.
5471 ///
5472 /// {@macro flutter.widgets.navigator.canPop}
5473 ///
5474 /// See also:
5475 ///
5476 /// * [Route.isFirst], which returns true for routes for which [canPop]
5477 /// returns false.
5478 bool canPop() {
5479 final Iterator<_RouteEntry> iterator = _history.where(_RouteEntry.isPresentPredicate).iterator;
5480 if (!iterator.moveNext()) {
5481 // We have no active routes, so we can't pop.
5482 return false;
5483 }
5484 if (iterator.current.route.willHandlePopInternally) {
5485 // The first route can handle pops itself, so we can pop.
5486 return true;
5487 }
5488 if (!iterator.moveNext()) {
5489 // There's only one route, so we can't pop.
5490 return false;
5491 }
5492 return true; // there's at least two routes, so we can pop
5493 }
5494
5495 /// Consults the current route's [Route.popDisposition] method, and acts
5496 /// accordingly, potentially popping the route as a result; returns whether
5497 /// the pop request should be considered handled.
5498 ///
5499 /// {@macro flutter.widgets.navigator.maybePop}
5500 ///
5501 /// See also:
5502 ///
5503 /// * [Form], which provides a [Form.canPop] boolean that enables the
5504 /// form to prevent any [pop]s initiated by the app's back button.
5505 /// * [ModalRoute], which provides a `scopedOnPopCallback` that can be used
5506 /// to define the route's `willPop` method.
5507 @optionalTypeArgs
5508 Future<bool> maybePop<T extends Object?>([T? result]) async {
5509 final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
5510 if (lastEntry == null) {
5511 return false;
5512 }
5513 assert(lastEntry.route._navigator == this);
5514
5515 // TODO(justinmc): When the deprecated willPop method is removed, delete
5516 // this code and use only popDisposition, below.
5517 if (await lastEntry.route.willPop() == RoutePopDisposition.doNotPop) {
5518 return true;
5519 }
5520 if (!mounted) {
5521 // Forget about this pop, we were disposed in the meantime.
5522 return true;
5523 }
5524
5525 final _RouteEntry? newLastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
5526 if (lastEntry != newLastEntry) {
5527 // Forget about this pop, something happened to our history in the meantime.
5528 return true;
5529 }
5530
5531 switch (lastEntry.route.popDisposition) {
5532 case RoutePopDisposition.bubble:
5533 return false;
5534 case RoutePopDisposition.pop:
5535 pop(result);
5536 return true;
5537 case RoutePopDisposition.doNotPop:
5538 lastEntry.route.onPopInvokedWithResult(false, result);
5539 return true;
5540 }
5541 }
5542
5543 /// Pop the top-most route off the navigator.
5544 ///
5545 /// {@macro flutter.widgets.navigator.pop}
5546 ///
5547 /// {@tool snippet}
5548 ///
5549 /// Typical usage for closing a route is as follows:
5550 ///
5551 /// ```dart
5552 /// void _handleClose() {
5553 /// navigator.pop();
5554 /// }
5555 /// ```
5556 /// {@end-tool}
5557 /// {@tool snippet}
5558 ///
5559 /// A dialog box might be closed with a result:
5560 ///
5561 /// ```dart
5562 /// void _handleAccept() {
5563 /// navigator.pop(true); // dialog returns true
5564 /// }
5565 /// ```
5566 /// {@end-tool}
5567 @optionalTypeArgs
5568 void pop<T extends Object?>([T? result]) {
5569 assert(!_debugLocked);
5570 assert(() {
5571 _debugLocked = true;
5572 return true;
5573 }());
5574 final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
5575 if (entry.pageBased && widget.onPopPage != null) {
5576 if (widget.onPopPage!(entry.route, result)) {
5577 if (entry.currentState.index <= _RouteLifecycle.idle.index) {
5578 // The entry may have been disposed if the pop finishes synchronously.
5579 assert(entry.route._popCompleter.isCompleted);
5580 entry.currentState = _RouteLifecycle.pop;
5581 }
5582 entry.route.onPopInvokedWithResult(true, result);
5583 }
5584 } else {
5585 entry.pop<T>(result);
5586 assert(entry.currentState == _RouteLifecycle.pop);
5587 }
5588 if (entry.currentState == _RouteLifecycle.pop) {
5589 _flushHistoryUpdates(rearrangeOverlay: false);
5590 }
5591 assert(entry.currentState == _RouteLifecycle.idle || entry.route._popCompleter.isCompleted);
5592 assert(() {
5593 _debugLocked = false;
5594 return true;
5595 }());
5596 _afterNavigation(entry.route);
5597 }
5598
5599 /// Calls [pop] repeatedly until the predicate returns true.
5600 ///
5601 /// {@macro flutter.widgets.navigator.popUntil}
5602 ///
5603 /// {@tool snippet}
5604 ///
5605 /// Typical usage is as follows:
5606 ///
5607 /// ```dart
5608 /// void _doLogout() {
5609 /// navigator.popUntil(ModalRoute.withName('/login'));
5610 /// }
5611 /// ```
5612 /// {@end-tool}
5613 void popUntil(RoutePredicate predicate) {
5614 _RouteEntry? candidate = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
5615 while (candidate != null) {
5616 if (predicate(candidate.route)) {
5617 return;
5618 }
5619 pop();
5620 candidate = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
5621 }
5622 }
5623
5624 /// Immediately remove `route` from the navigator, and [Route.dispose] it.
5625 ///
5626 /// {@macro flutter.widgets.navigator.removeRoute}
5627 @optionalTypeArgs
5628 void removeRoute<T extends Object?>(Route<T> route, [T? result]) {
5629 assert(!_debugLocked);
5630 assert(() {
5631 _debugLocked = true;
5632 return true;
5633 }());
5634 assert(route._navigator == this);
5635 final bool wasCurrent = route.isCurrent;
5636 final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
5637 entry.complete(result);
5638 _flushHistoryUpdates(rearrangeOverlay: false);
5639 assert(() {
5640 _debugLocked = false;
5641 return true;
5642 }());
5643 if (wasCurrent) {
5644 _afterNavigation(_lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate)?.route);
5645 }
5646 }
5647
5648 /// Immediately remove a route from the navigator, and [Route.dispose] it. The
5649 /// route to be removed is the one below the given `anchorRoute`.
5650 ///
5651 /// {@macro flutter.widgets.navigator.removeRouteBelow}
5652 @optionalTypeArgs
5653 void removeRouteBelow<T extends Object?>(Route<T> anchorRoute, [T? result]) {
5654 assert(!_debugLocked);
5655 assert(() {
5656 _debugLocked = true;
5657 return true;
5658 }());
5659 assert(anchorRoute._navigator == this);
5660 final int anchorIndex = _history.indexWhere(_RouteEntry.isRoutePredicate(anchorRoute));
5661 assert(anchorIndex >= 0, 'This Navigator does not contain the specified anchorRoute.');
5662 assert(
5663 _history[anchorIndex].isPresent,
5664 'The specified anchorRoute has already been removed from the Navigator.',
5665 );
5666 int index = anchorIndex - 1;
5667 while (index >= 0) {
5668 if (_history[index].isPresent) {
5669 break;
5670 }
5671 index -= 1;
5672 }
5673 assert(index >= 0, 'There are no routes below the specified anchorRoute.');
5674 _history[index].complete(result);
5675 _flushHistoryUpdates(rearrangeOverlay: false);
5676 assert(() {
5677 _debugLocked = false;
5678 return true;
5679 }());
5680 }
5681
5682 /// Complete the lifecycle for a route that has been popped off the navigator.
5683 ///
5684 /// When the navigator pops a route, the navigator retains a reference to the
5685 /// route in order to call [Route.dispose] if the navigator itself is removed
5686 /// from the tree. When the route is finished with any exit animation, the
5687 /// route should call this function to complete its lifecycle (e.g., to
5688 /// receive a call to [Route.dispose]).
5689 ///
5690 /// The given `route` must have already received a call to [Route.didPop].
5691 /// This function may be called directly from [Route.didPop] if [Route.didPop]
5692 /// will return true.
5693 void finalizeRoute(Route<dynamic> route) {
5694 // FinalizeRoute may have been called while we were already locked as a
5695 // responds to route.didPop(). Make sure to leave in the state we were in
5696 // before the call.
5697 bool? wasDebugLocked;
5698 assert(() {
5699 wasDebugLocked = _debugLocked;
5700 _debugLocked = true;
5701 return true;
5702 }());
5703 assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
5704 final int index = _history.indexWhere(_RouteEntry.isRoutePredicate(route));
5705 final _RouteEntry entry = _history[index];
5706 // For page-based route with zero transition, the finalizeRoute can be
5707 // called on any life cycle above pop.
5708 if (entry.pageBased && entry.currentState.index < _RouteLifecycle.pop.index) {
5709 _observedRouteDeletions.add(
5710 _NavigatorPopObservation(
5711 route,
5712 _getRouteBefore(index - 1, _RouteEntry.willBePresentPredicate)?.route,
5713 ),
5714 );
5715 } else {
5716 assert(entry.currentState == _RouteLifecycle.popping);
5717 }
5718 entry.finalize();
5719 // finalizeRoute can be called during _flushHistoryUpdates if a pop
5720 // finishes synchronously.
5721 if (!_flushingHistory) {
5722 _flushHistoryUpdates(rearrangeOverlay: false);
5723 }
5724
5725 assert(() {
5726 _debugLocked = wasDebugLocked!;
5727 return true;
5728 }());
5729 }
5730
5731 @optionalTypeArgs
5732 Route<T>? _getRouteById<T>(String id) {
5733 return _firstRouteEntryWhereOrNull((_RouteEntry entry) => entry.restorationId == id)?.route
5734 as Route<T>?;
5735 }
5736
5737 int get _userGesturesInProgress => _userGesturesInProgressCount;
5738 int _userGesturesInProgressCount = 0;
5739 set _userGesturesInProgress(int value) {
5740 _userGesturesInProgressCount = value;
5741 userGestureInProgressNotifier.value = _userGesturesInProgress > 0;
5742 }
5743
5744 /// Whether a route is currently being manipulated by the user, e.g.
5745 /// as during an iOS back gesture.
5746 ///
5747 /// See also:
5748 ///
5749 /// * [userGestureInProgressNotifier], which notifies its listeners if
5750 /// the value of [userGestureInProgress] changes.
5751 bool get userGestureInProgress => userGestureInProgressNotifier.value;
5752
5753 /// Notifies its listeners if the value of [userGestureInProgress] changes.
5754 final ValueNotifier<bool> userGestureInProgressNotifier = ValueNotifier<bool>(false);
5755
5756 /// The navigator is being controlled by a user gesture.
5757 ///
5758 /// For example, called when the user beings an iOS back gesture.
5759 ///
5760 /// When the gesture finishes, call [didStopUserGesture].
5761 void didStartUserGesture() {
5762 _userGesturesInProgress += 1;
5763 if (_userGesturesInProgress == 1) {
5764 final int routeIndex = _getIndexBefore(
5765 _history.length - 1,
5766 _RouteEntry.willBePresentPredicate,
5767 );
5768 final Route<dynamic> route = _history[routeIndex].route;
5769 Route<dynamic>? previousRoute;
5770 if (!route.willHandlePopInternally && routeIndex > 0) {
5771 previousRoute = _getRouteBefore(routeIndex - 1, _RouteEntry.willBePresentPredicate)!.route;
5772 }
5773 for (final NavigatorObserver observer in _effectiveObservers) {
5774 observer.didStartUserGesture(route, previousRoute);
5775 }
5776 }
5777 }
5778
5779 /// A user gesture completed.
5780 ///
5781 /// Notifies the navigator that a gesture regarding which the navigator was
5782 /// previously notified with [didStartUserGesture] has completed.
5783 void didStopUserGesture() {
5784 assert(_userGesturesInProgress > 0);
5785 _userGesturesInProgress -= 1;
5786 if (_userGesturesInProgress == 0) {
5787 for (final NavigatorObserver observer in _effectiveObservers) {
5788 observer.didStopUserGesture();
5789 }
5790 }
5791 }
5792
5793 final Set<int> _activePointers = <int>{};
5794
5795 void _handlePointerDown(PointerDownEvent event) {
5796 _activePointers.add(event.pointer);
5797 }
5798
5799 void _handlePointerUpOrCancel(PointerEvent event) {
5800 _activePointers.remove(event.pointer);
5801 }
5802
5803 void _cancelActivePointers() {
5804 // TODO(abarth): This mechanism is far from perfect. See https://github.com/flutter/flutter/issues/4770
5805 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
5806 // If we're between frames (SchedulerPhase.idle) then absorb any
5807 // subsequent pointers from this frame. The absorbing flag will be
5808 // reset in the next frame, see build().
5809 final RenderAbsorbPointer? absorber = _overlayKey.currentContext
5810 ?.findAncestorRenderObjectOfType<RenderAbsorbPointer>();
5811 setState(() {
5812 absorber?.absorbing = true;
5813 // We do this in setState so that we'll reset the absorbing value back
5814 // to false on the next frame.
5815 });
5816 }
5817 _activePointers.toList().forEach(WidgetsBinding.instance.cancelPointer);
5818 }
5819
5820 /// Gets first route entry satisfying the predicate, or null if not found.
5821 _RouteEntry? _firstRouteEntryWhereOrNull(_RouteEntryPredicate test) {
5822 for (final _RouteEntry element in _history) {
5823 if (test(element)) {
5824 return element;
5825 }
5826 }
5827 return null;
5828 }
5829
5830 /// Gets last route entry satisfying the predicate, or null if not found.
5831 _RouteEntry? _lastRouteEntryWhereOrNull(_RouteEntryPredicate test) {
5832 _RouteEntry? result;
5833 for (final _RouteEntry element in _history) {
5834 if (test(element)) {
5835 result = element;
5836 }
5837 }
5838 return result;
5839 }
5840
5841 @protected
5842 @override
5843 Widget build(BuildContext context) {
5844 assert(!_debugLocked);
5845 assert(_history.isNotEmpty);
5846
5847 // Hides the HeroControllerScope for the widget subtree so that the other
5848 // nested navigator underneath will not pick up the hero controller above
5849 // this level.
5850 return HeroControllerScope.none(
5851 child: NotificationListener<NavigationNotification>(
5852 onNotification: (NavigationNotification notification) {
5853 // If the state of this Navigator does not change whether or not the
5854 // whole framework can pop, propagate the Notification as-is.
5855 if (notification.canHandlePop || !canPop()) {
5856 return false;
5857 }
5858 // Otherwise, dispatch a new Notification with the correct canPop and
5859 // stop the propagation of the old Notification.
5860 const NavigationNotification nextNotification = NavigationNotification(
5861 canHandlePop: true,
5862 );
5863 nextNotification.dispatch(context);
5864 return true;
5865 },
5866 child: Listener(
5867 onPointerDown: _handlePointerDown,
5868 onPointerUp: _handlePointerUpOrCancel,
5869 onPointerCancel: _handlePointerUpOrCancel,
5870 child: AbsorbPointer(
5871 absorbing: false, // it's mutated directly by _cancelActivePointers above
5872 child: FocusTraversalGroup(
5873 policy: FocusTraversalGroup.maybeOf(context),
5874 child: Focus(
5875 focusNode: focusNode,
5876 autofocus: true,
5877 skipTraversal: true,
5878 includeSemantics: false,
5879 child: UnmanagedRestorationScope(
5880 bucket: bucket,
5881 child: Overlay(
5882 key: _overlayKey,
5883 clipBehavior: widget.clipBehavior,
5884 initialEntries: overlay == null
5885 ? _allRouteOverlayEntries.toList(growable: false)
5886 : const <OverlayEntry>[],
5887 ),
5888 ),
5889 ),
5890 ),
5891 ),
5892 ),
5893 ),
5894 );
5895 }
5896}
5897
5898enum _RouteRestorationType { named, anonymous }
5899
5900abstract class _RestorationInformation {
5901 _RestorationInformation(this.type);
5902 factory _RestorationInformation.named({
5903 required String name,
5904 required Object? arguments,
5905 required int restorationScopeId,
5906 }) = _NamedRestorationInformation;
5907 factory _RestorationInformation.anonymous({
5908 required RestorableRouteBuilder<Object?> routeBuilder,
5909 required Object? arguments,
5910 required int restorationScopeId,
5911 }) = _AnonymousRestorationInformation;
5912
5913 factory _RestorationInformation.fromSerializableData(Object data) {
5914 final List<Object?> casted = data as List<Object?>;
5915 assert(casted.isNotEmpty);
5916 final _RouteRestorationType type = _RouteRestorationType.values[casted[0]! as int];
5917 switch (type) {
5918 case _RouteRestorationType.named:
5919 return _NamedRestorationInformation.fromSerializableData(casted.sublist(1));
5920 case _RouteRestorationType.anonymous:
5921 return _AnonymousRestorationInformation.fromSerializableData(casted.sublist(1));
5922 }
5923 }
5924
5925 final _RouteRestorationType type;
5926 int get restorationScopeId;
5927 Object? _serializableData;
5928
5929 bool get isRestorable => true;
5930
5931 Object getSerializableData() {
5932 _serializableData ??= computeSerializableData();
5933 return _serializableData!;
5934 }
5935
5936 @mustCallSuper
5937 List<Object> computeSerializableData() {
5938 return <Object>[type.index];
5939 }
5940
5941 @protected
5942 Route<dynamic> createRoute(NavigatorState navigator);
5943
5944 _RouteEntry toRouteEntry(
5945 NavigatorState navigator, {
5946 _RouteLifecycle initialState = _RouteLifecycle.add,
5947 }) {
5948 final Route<Object?> route = createRoute(navigator);
5949 return _RouteEntry(
5950 route,
5951 pageBased: false,
5952 initialState: initialState,
5953 restorationInformation: this,
5954 );
5955 }
5956}
5957
5958class _NamedRestorationInformation extends _RestorationInformation {
5959 _NamedRestorationInformation({
5960 required this.name,
5961 required this.arguments,
5962 required this.restorationScopeId,
5963 }) : super(_RouteRestorationType.named);
5964
5965 _NamedRestorationInformation.fromSerializableData(List<Object?> data)
5966 : assert(data.length > 1),
5967 restorationScopeId = data[0]! as int,
5968 name = data[1]! as String,
5969 arguments = data.elementAtOrNull(2),
5970 super(_RouteRestorationType.named);
5971
5972 @override
5973 List<Object> computeSerializableData() {
5974 return super.computeSerializableData()
5975 ..addAll(<Object>[restorationScopeId, name, if (arguments != null) arguments!]);
5976 }
5977
5978 @override
5979 final int restorationScopeId;
5980 final String name;
5981 final Object? arguments;
5982
5983 @override
5984 Route<dynamic> createRoute(NavigatorState navigator) {
5985 final Route<dynamic> route = navigator._routeNamed<dynamic>(name, arguments: arguments)!;
5986 return route;
5987 }
5988}
5989
5990class _AnonymousRestorationInformation extends _RestorationInformation {
5991 _AnonymousRestorationInformation({
5992 required this.routeBuilder,
5993 required this.arguments,
5994 required this.restorationScopeId,
5995 }) : super(_RouteRestorationType.anonymous);
5996
5997 _AnonymousRestorationInformation.fromSerializableData(List<Object?> data)
5998 : assert(data.length > 1),
5999 restorationScopeId = data[0]! as int,
6000 routeBuilder =
6001 ui.PluginUtilities.getCallbackFromHandle(
6002 ui.CallbackHandle.fromRawHandle(data[1]! as int),
6003 )!
6004 as RestorableRouteBuilder,
6005 arguments = data.elementAtOrNull(2),
6006 super(_RouteRestorationType.anonymous);
6007
6008 @override
6009 // TODO(goderbauer): remove the kIsWeb check when https://github.com/flutter/flutter/issues/33615 is resolved.
6010 bool get isRestorable => !kIsWeb;
6011
6012 @override
6013 List<Object> computeSerializableData() {
6014 assert(isRestorable);
6015 final ui.CallbackHandle? handle = ui.PluginUtilities.getCallbackHandle(routeBuilder);
6016 assert(handle != null);
6017 return super.computeSerializableData()..addAll(<Object>[
6018 restorationScopeId,
6019 handle!.toRawHandle(),
6020 if (arguments != null) arguments!,
6021 ]);
6022 }
6023
6024 @override
6025 final int restorationScopeId;
6026 final RestorableRouteBuilder<Object?> routeBuilder;
6027 final Object? arguments;
6028
6029 @override
6030 Route<dynamic> createRoute(NavigatorState navigator) {
6031 final Route<dynamic> result = routeBuilder(navigator.context, arguments);
6032 return result;
6033 }
6034}
6035
6036class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
6037 // Routes not associated with a page are stored under key 'null'.
6038 Map<String?, List<Object>>? _pageToPagelessRoutes;
6039
6040 // Updating.
6041
6042 void update(_History history) {
6043 assert(isRegistered);
6044 final bool wasUninitialized = _pageToPagelessRoutes == null;
6045 bool needsSerialization = wasUninitialized;
6046 _pageToPagelessRoutes ??= <String, List<Object>>{};
6047 _RouteEntry? currentPage;
6048 List<Object> newRoutesForCurrentPage = <Object>[];
6049 List<Object> oldRoutesForCurrentPage = _pageToPagelessRoutes![null] ?? const <Object>[];
6050 bool restorationEnabled = true;
6051
6052 final Map<String?, List<Object>> newMap = <String?, List<Object>>{};
6053 final Set<String?> removedPages = _pageToPagelessRoutes!.keys.toSet();
6054
6055 for (final _RouteEntry entry in history) {
6056 if (!entry.isPresentForRestoration) {
6057 entry.restorationEnabled = false;
6058 continue;
6059 }
6060
6061 assert(entry.isPresentForRestoration);
6062 if (entry.pageBased) {
6063 needsSerialization =
6064 needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
6065 _finalizeEntry(newRoutesForCurrentPage, currentPage, newMap, removedPages);
6066 currentPage = entry;
6067 restorationEnabled = entry.restorationId != null;
6068 entry.restorationEnabled = restorationEnabled;
6069 if (restorationEnabled) {
6070 assert(entry.restorationId != null);
6071 newRoutesForCurrentPage = <Object>[];
6072 oldRoutesForCurrentPage = _pageToPagelessRoutes![entry.restorationId] ?? const <Object>[];
6073 } else {
6074 newRoutesForCurrentPage = const <Object>[];
6075 oldRoutesForCurrentPage = const <Object>[];
6076 }
6077 continue;
6078 }
6079
6080 assert(!entry.pageBased);
6081 restorationEnabled =
6082 restorationEnabled && (entry.restorationInformation?.isRestorable ?? false);
6083 entry.restorationEnabled = restorationEnabled;
6084 if (restorationEnabled) {
6085 assert(entry.restorationId != null);
6086 assert(currentPage == null || currentPage.restorationId != null);
6087 assert(entry.restorationInformation != null);
6088 final Object serializedData = entry.restorationInformation!.getSerializableData();
6089 needsSerialization =
6090 needsSerialization ||
6091 oldRoutesForCurrentPage.length <= newRoutesForCurrentPage.length ||
6092 oldRoutesForCurrentPage[newRoutesForCurrentPage.length] != serializedData;
6093 newRoutesForCurrentPage.add(serializedData);
6094 }
6095 }
6096 needsSerialization =
6097 needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
6098 _finalizeEntry(newRoutesForCurrentPage, currentPage, newMap, removedPages);
6099
6100 needsSerialization = needsSerialization || removedPages.isNotEmpty;
6101
6102 assert(
6103 wasUninitialized || _debugMapsEqual(_pageToPagelessRoutes!, newMap) != needsSerialization,
6104 );
6105
6106 if (needsSerialization) {
6107 _pageToPagelessRoutes = newMap;
6108 notifyListeners();
6109 }
6110 }
6111
6112 void _finalizeEntry(
6113 List<Object> routes,
6114 _RouteEntry? page,
6115 Map<String?, List<Object>> pageToRoutes,
6116 Set<String?> pagesToRemove,
6117 ) {
6118 assert(page == null || page.pageBased);
6119 assert(!pageToRoutes.containsKey(page?.restorationId));
6120 if (routes.isNotEmpty) {
6121 assert(page == null || page.restorationId != null);
6122 final String? restorationId = page?.restorationId;
6123 pageToRoutes[restorationId] = routes;
6124 pagesToRemove.remove(restorationId);
6125 }
6126 }
6127
6128 bool _debugMapsEqual(Map<String?, List<Object>> a, Map<String?, List<Object>> b) {
6129 if (!setEquals(a.keys.toSet(), b.keys.toSet())) {
6130 return false;
6131 }
6132 for (final String? key in a.keys) {
6133 if (!listEquals(a[key], b[key])) {
6134 return false;
6135 }
6136 }
6137 return true;
6138 }
6139
6140 void clear() {
6141 assert(isRegistered);
6142 if (_pageToPagelessRoutes == null) {
6143 return;
6144 }
6145 _pageToPagelessRoutes = null;
6146 notifyListeners();
6147 }
6148
6149 // Restoration.
6150
6151 bool get hasData => _pageToPagelessRoutes != null;
6152
6153 List<_RouteEntry> restoreEntriesForPage(_RouteEntry? page, NavigatorState navigator) {
6154 assert(isRegistered);
6155 assert(page == null || page.pageBased);
6156 final List<_RouteEntry> result = <_RouteEntry>[];
6157 if (_pageToPagelessRoutes == null || (page != null && page.restorationId == null)) {
6158 return result;
6159 }
6160 final List<Object>? serializedData = _pageToPagelessRoutes![page?.restorationId];
6161 if (serializedData == null) {
6162 return result;
6163 }
6164 for (final Object data in serializedData) {
6165 result.add(_RestorationInformation.fromSerializableData(data).toRouteEntry(navigator));
6166 }
6167 return result;
6168 }
6169
6170 // RestorableProperty overrides.
6171
6172 @override
6173 Map<String?, List<Object>>? createDefaultValue() {
6174 return null;
6175 }
6176
6177 @override
6178 Map<String?, List<Object>>? fromPrimitives(Object? data) {
6179 final Map<dynamic, dynamic> casted = data! as Map<dynamic, dynamic>;
6180 return casted.map<String?, List<Object>>(
6181 (dynamic key, dynamic value) => MapEntry<String?, List<Object>>(
6182 key as String?,
6183 List<Object>.from(value as List<dynamic>),
6184 ),
6185 );
6186 }
6187
6188 @override
6189 void initWithValue(Map<String?, List<Object>>? value) {
6190 _pageToPagelessRoutes = value;
6191 }
6192
6193 @override
6194 Object? toPrimitives() {
6195 return _pageToPagelessRoutes;
6196 }
6197
6198 @override
6199 bool get enabled => hasData;
6200}
6201
6202/// A callback that given a [BuildContext] finds a [NavigatorState].
6203///
6204/// Used by [RestorableRouteFuture.navigatorFinder] to determine the navigator
6205/// to which a new route should be added.
6206typedef NavigatorFinderCallback = NavigatorState Function(BuildContext context);
6207
6208/// A callback that given some `arguments` and a `navigator` adds a new
6209/// restorable route to that `navigator` and returns the opaque ID of that
6210/// new route.
6211///
6212/// Usually, this callback calls one of the imperative methods on the Navigator
6213/// that have "restorable" in the name and returns their return value.
6214///
6215/// Used by [RestorableRouteFuture.onPresent].
6216typedef RoutePresentationCallback = String Function(NavigatorState navigator, Object? arguments);
6217
6218/// A callback to handle the result of a completed [Route].
6219///
6220/// The return value of the route (which can be null for e.g. void routes) is
6221/// passed to the callback.
6222///
6223/// Used by [RestorableRouteFuture.onComplete].
6224typedef RouteCompletionCallback<T> = void Function(T result);
6225
6226/// Gives access to a [Route] object and its return value that was added to a
6227/// navigator via one of its "restorable" API methods.
6228///
6229/// When a [State] object wants access to the return value of a [Route] object
6230/// it has pushed onto the [Navigator], a [RestorableRouteFuture] ensures that
6231/// it will also have access to that value after state restoration.
6232///
6233/// To show a new route on the navigator defined by the [navigatorFinder], call
6234/// [present], which will invoke the [onPresent] callback. The [onPresent]
6235/// callback must add a new route to the navigator provided to it using one
6236/// of the "restorable" API methods. When the newly added route completes, the
6237/// [onComplete] callback executes. It is given the return value of the route,
6238/// which may be null.
6239///
6240/// While the route added via [present] is shown on the navigator, it can be
6241/// accessed via the [route] getter.
6242///
6243/// If the property is restored to a state in which [present] had been called on
6244/// it, but the route has not completed yet, the [RestorableRouteFuture] will
6245/// obtain the restored route object from the navigator again and call
6246/// [onComplete] once it completes.
6247///
6248/// The [RestorableRouteFuture] can only keep track of one active [route].
6249/// When [present] has been called to add a route, it may only be called again
6250/// after the previously added route has completed.
6251///
6252/// {@tool dartpad}
6253/// This example uses a [RestorableRouteFuture] in the `_MyHomeState` to push a
6254/// new `MyCounter` route and to retrieve its return value.
6255///
6256/// ** See code in examples/api/lib/widgets/navigator/restorable_route_future.0.dart **
6257/// {@end-tool}
6258class RestorableRouteFuture<T> extends RestorableProperty<String?> {
6259 /// Creates a [RestorableRouteFuture].
6260 RestorableRouteFuture({
6261 this.navigatorFinder = _defaultNavigatorFinder,
6262 required this.onPresent,
6263 this.onComplete,
6264 });
6265
6266 /// A callback that given the [BuildContext] of the [State] object to which
6267 /// this property is registered returns the [NavigatorState] of the navigator
6268 /// to which the route instantiated in [onPresent] is added.
6269 final NavigatorFinderCallback navigatorFinder;
6270
6271 /// A callback that add a new [Route] to the provided navigator.
6272 ///
6273 /// The callback must use one of the API methods on the [NavigatorState] that
6274 /// have "restorable" in their name (e.g. [NavigatorState.restorablePush],
6275 /// [NavigatorState.restorablePushNamed], etc.) and return the opaque ID
6276 /// returned by those methods.
6277 ///
6278 /// This callback is invoked when [present] is called with the `arguments`
6279 /// Object that was passed to that method and the [NavigatorState] obtained
6280 /// from [navigatorFinder].
6281 final RoutePresentationCallback onPresent;
6282
6283 /// A callback that is invoked when the [Route] added via [onPresent]
6284 /// completes.
6285 ///
6286 /// The return value of that route is passed to this method.
6287 final RouteCompletionCallback<T>? onComplete;
6288
6289 /// Shows the route created by [onPresent] and invoke [onComplete] when it
6290 /// completes.
6291 ///
6292 /// The `arguments` object is passed to [onPresent] and can be used to
6293 /// customize the route. It must be serializable via the
6294 /// [StandardMessageCodec]. Often, a [Map] is used to pass key-value pairs.
6295 void present([Object? arguments]) {
6296 assert(!isPresent);
6297 assert(isRegistered);
6298 final String routeId = onPresent(_navigator, arguments);
6299 _hookOntoRouteFuture(routeId);
6300 notifyListeners();
6301 }
6302
6303 /// Whether the [Route] created by [present] is currently shown.
6304 ///
6305 /// Returns true after [present] has been called until the [Route] completes.
6306 bool get isPresent => route != null;
6307
6308 /// The route that [present] added to the Navigator.
6309 ///
6310 /// Returns null when currently no route is shown
6311 Route<T>? get route => _route;
6312 Route<T>? _route;
6313
6314 @override
6315 String? createDefaultValue() => null;
6316
6317 @override
6318 void initWithValue(String? value) {
6319 if (value != null) {
6320 _hookOntoRouteFuture(value);
6321 }
6322 }
6323
6324 @override
6325 Object? toPrimitives() {
6326 assert(route != null);
6327 assert(enabled);
6328 return route?.restorationScopeId.value;
6329 }
6330
6331 @override
6332 String fromPrimitives(Object? data) {
6333 assert(data != null);
6334 return data! as String;
6335 }
6336
6337 bool _disposed = false;
6338
6339 @override
6340 void dispose() {
6341 super.dispose();
6342 _route?.restorationScopeId.removeListener(notifyListeners);
6343 _disposed = true;
6344 }
6345
6346 @override
6347 bool get enabled => route?.restorationScopeId.value != null;
6348
6349 NavigatorState get _navigator {
6350 final NavigatorState navigator = navigatorFinder(state.context);
6351 return navigator;
6352 }
6353
6354 void _hookOntoRouteFuture(String id) {
6355 _route = _navigator._getRouteById<T>(id);
6356 assert(_route != null);
6357 route!.restorationScopeId.addListener(notifyListeners);
6358 route!.popped.then((dynamic result) {
6359 if (_disposed) {
6360 return;
6361 }
6362 _route?.restorationScopeId.removeListener(notifyListeners);
6363 _route = null;
6364 notifyListeners();
6365 onComplete?.call(result as T);
6366 });
6367 }
6368
6369 static NavigatorState _defaultNavigatorFinder(BuildContext context) => Navigator.of(context);
6370}
6371
6372/// A notification that a change in navigation has taken place.
6373///
6374/// Specifically, this notification indicates that at least one of the following
6375/// has occurred:
6376///
6377/// * That route stack of a [Navigator] has changed in any way.
6378/// * The ability to pop has changed, such as controlled by [PopScope].
6379class NavigationNotification extends Notification {
6380 /// Creates a notification that some change in navigation has happened.
6381 const NavigationNotification({required this.canHandlePop});
6382
6383 /// Indicates that the originator of this [Notification] is capable of
6384 /// handling a navigation pop.
6385 final bool canHandlePop;
6386
6387 @override
6388 String toString() {
6389 return 'NavigationNotification canHandlePop: $canHandlePop';
6390 }
6391}
6392