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
5import 'dart:async';
6import 'dart:ui' as ui;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/rendering.dart';
10import 'package:flutter/scheduler.dart';
11import 'package:flutter/services.dart';
12
13import 'actions.dart';
14import 'basic.dart';
15import 'display_feature_sub_screen.dart';
16import 'focus_manager.dart';
17import 'focus_scope.dart';
18import 'focus_traversal.dart';
19import 'framework.dart';
20import 'modal_barrier.dart';
21import 'navigator.dart';
22import 'overlay.dart';
23import 'page_storage.dart';
24import 'primary_scroll_controller.dart';
25import 'restoration.dart';
26import 'scroll_controller.dart';
27import 'transitions.dart';
28
29// Examples can assume:
30// late NavigatorState navigator;
31// late BuildContext context;
32// Future askTheUserIfTheyAreSure() async { return true; }
33// abstract class MyWidget extends StatefulWidget { const MyWidget({super.key}); }
34// late dynamic _myState, newValue;
35// late StateSetter setState;
36
37/// A route that displays widgets in the [Navigator]'s [Overlay].
38///
39/// See also:
40///
41/// * [Route], which documents the meaning of the `T` generic type argument.
42abstract class OverlayRoute<T> extends Route<T> {
43 /// Creates a route that knows how to interact with an [Overlay].
44 OverlayRoute({
45 super.settings,
46 });
47
48 /// Subclasses should override this getter to return the builders for the overlay.
49 @factory
50 Iterable<OverlayEntry> createOverlayEntries();
51
52 @override
53 List<OverlayEntry> get overlayEntries => _overlayEntries;
54 final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
55
56 @override
57 void install() {
58 assert(_overlayEntries.isEmpty);
59 _overlayEntries.addAll(createOverlayEntries());
60 super.install();
61 }
62
63 /// Controls whether [didPop] calls [NavigatorState.finalizeRoute].
64 ///
65 /// If true, this route removes its overlay entries during [didPop].
66 /// Subclasses can override this getter if they want to delay finalization
67 /// (for example to animate the route's exit before removing it from the
68 /// overlay).
69 ///
70 /// Subclasses that return false from [finishedWhenPopped] are responsible for
71 /// calling [NavigatorState.finalizeRoute] themselves.
72 @protected
73 bool get finishedWhenPopped => true;
74
75 @override
76 bool didPop(T? result) {
77 final bool returnValue = super.didPop(result);
78 assert(returnValue);
79 if (finishedWhenPopped) {
80 navigator!.finalizeRoute(this);
81 }
82 return returnValue;
83 }
84
85 @override
86 void dispose() {
87 for (final OverlayEntry entry in _overlayEntries) {
88 entry.dispose();
89 }
90 _overlayEntries.clear();
91 super.dispose();
92 }
93}
94
95/// A route with entrance and exit transitions.
96///
97/// See also:
98///
99/// * [Route], which documents the meaning of the `T` generic type argument.
100abstract class TransitionRoute<T> extends OverlayRoute<T> {
101 /// Creates a route that animates itself when it is pushed or popped.
102 TransitionRoute({
103 super.settings,
104 });
105
106 /// This future completes only once the transition itself has finished, after
107 /// the overlay entries have been removed from the navigator's overlay.
108 ///
109 /// This future completes once the animation has been dismissed. That will be
110 /// after [popped], because [popped] typically completes before the animation
111 /// even starts, as soon as the route is popped.
112 Future<T?> get completed => _transitionCompleter.future;
113 final Completer<T?> _transitionCompleter = Completer<T?>();
114
115 /// Handle to the performance mode request.
116 ///
117 /// When the route is animating, the performance mode is requested. It is then
118 /// disposed when the animation ends. Requesting [DartPerformanceMode.latency]
119 /// indicates to the engine that the transition is latency sensitive and to delay
120 /// non-essential work while this handle is active.
121 PerformanceModeRequestHandle? _performanceModeRequestHandle;
122
123 /// {@template flutter.widgets.TransitionRoute.transitionDuration}
124 /// The duration the transition going forwards.
125 ///
126 /// See also:
127 ///
128 /// * [reverseTransitionDuration], which controls the duration of the
129 /// transition when it is in reverse.
130 /// {@endtemplate}
131 Duration get transitionDuration;
132
133 /// {@template flutter.widgets.TransitionRoute.reverseTransitionDuration}
134 /// The duration the transition going in reverse.
135 ///
136 /// By default, the reverse transition duration is set to the value of
137 /// the forwards [transitionDuration].
138 /// {@endtemplate}
139 Duration get reverseTransitionDuration => transitionDuration;
140
141 /// {@template flutter.widgets.TransitionRoute.opaque}
142 /// Whether the route obscures previous routes when the transition is complete.
143 ///
144 /// When an opaque route's entrance transition is complete, the routes behind
145 /// the opaque route will not be built to save resources.
146 /// {@endtemplate}
147 bool get opaque;
148
149 /// {@template flutter.widgets.TransitionRoute.allowSnapshotting}
150 /// Whether the route transition will prefer to animate a snapshot of the
151 /// entering/exiting routes.
152 ///
153 /// When this value is true, certain route transitions (such as the Android
154 /// zoom page transition) will snapshot the entering and exiting routes.
155 /// These snapshots are then animated in place of the underlying widgets to
156 /// improve performance of the transition.
157 ///
158 /// Generally this means that animations that occur on the entering/exiting
159 /// route while the route animation plays may appear frozen - unless they
160 /// are a hero animation or something that is drawn in a separate overlay.
161 /// {@endtemplate}
162 bool get allowSnapshotting => true;
163
164 // This ensures that if we got to the dismissed state while still current,
165 // we will still be disposed when we are eventually popped.
166 //
167 // This situation arises when dealing with the Cupertino dismiss gesture.
168 @override
169 bool get finishedWhenPopped => _controller!.status == AnimationStatus.dismissed && !_popFinalized;
170
171 bool _popFinalized = false;
172
173 /// The animation that drives the route's transition and the previous route's
174 /// forward transition.
175 Animation<double>? get animation => _animation;
176 Animation<double>? _animation;
177
178 /// The animation controller that the route uses to drive the transitions.
179 ///
180 /// The animation itself is exposed by the [animation] property.
181 @protected
182 AnimationController? get controller => _controller;
183 AnimationController? _controller;
184
185 /// The animation for the route being pushed on top of this route. This
186 /// animation lets this route coordinate with the entrance and exit transition
187 /// of route pushed on top of this route.
188 Animation<double>? get secondaryAnimation => _secondaryAnimation;
189 final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);
190
191 /// Whether to takeover the [controller] created by [createAnimationController].
192 ///
193 /// If true, this route will call [AnimationController.dispose] when the
194 /// controller is no longer needed.
195 /// If false, the controller should be disposed by whoever owned it.
196 ///
197 /// It defaults to `true`.
198 bool willDisposeAnimationController = true;
199
200 /// Called to create the animation controller that will drive the transitions to
201 /// this route from the previous one, and back to the previous route from this
202 /// one.
203 ///
204 /// The returned controller will be disposed by [AnimationController.dispose]
205 /// if the [willDisposeAnimationController] is `true`.
206 AnimationController createAnimationController() {
207 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
208 final Duration duration = transitionDuration;
209 final Duration reverseDuration = reverseTransitionDuration;
210 assert(duration >= Duration.zero);
211 return AnimationController(
212 duration: duration,
213 reverseDuration: reverseDuration,
214 debugLabel: debugLabel,
215 vsync: navigator!,
216 );
217 }
218
219 /// Called to create the animation that exposes the current progress of
220 /// the transition controlled by the animation controller created by
221 /// [createAnimationController()].
222 Animation<double> createAnimation() {
223 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
224 assert(_controller != null);
225 return _controller!.view;
226 }
227
228 T? _result;
229
230 void _handleStatusChanged(AnimationStatus status) {
231 switch (status) {
232 case AnimationStatus.completed:
233 if (overlayEntries.isNotEmpty) {
234 overlayEntries.first.opaque = opaque;
235 }
236 _performanceModeRequestHandle?.dispose();
237 _performanceModeRequestHandle = null;
238 case AnimationStatus.forward:
239 case AnimationStatus.reverse:
240 if (overlayEntries.isNotEmpty) {
241 overlayEntries.first.opaque = false;
242 }
243 _performanceModeRequestHandle ??=
244 SchedulerBinding.instance
245 .requestPerformanceMode(ui.DartPerformanceMode.latency);
246 case AnimationStatus.dismissed:
247 // We might still be an active route if a subclass is controlling the
248 // transition and hits the dismissed status. For example, the iOS
249 // back gesture drives this animation to the dismissed status before
250 // removing the route and disposing it.
251 if (!isActive) {
252 navigator!.finalizeRoute(this);
253 _popFinalized = true;
254 _performanceModeRequestHandle?.dispose();
255 _performanceModeRequestHandle = null;
256 }
257 }
258 }
259
260 @override
261 void install() {
262 assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.');
263 _controller = createAnimationController();
264 assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
265 _animation = createAnimation()
266 ..addStatusListener(_handleStatusChanged);
267 assert(_animation != null, '$runtimeType.createAnimation() returned null.');
268 super.install();
269 if (_animation!.isCompleted && overlayEntries.isNotEmpty) {
270 overlayEntries.first.opaque = opaque;
271 }
272 }
273
274 @override
275 TickerFuture didPush() {
276 assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
277 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
278 super.didPush();
279 return _controller!.forward();
280 }
281
282 @override
283 void didAdd() {
284 assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
285 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
286 super.didAdd();
287 _controller!.value = _controller!.upperBound;
288 }
289
290 @override
291 void didReplace(Route<dynamic>? oldRoute) {
292 assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().');
293 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
294 if (oldRoute is TransitionRoute) {
295 _controller!.value = oldRoute._controller!.value;
296 }
297 super.didReplace(oldRoute);
298 }
299
300 @override
301 bool didPop(T? result) {
302 assert(_controller != null, '$runtimeType.didPop called before calling install() or after calling dispose().');
303 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
304 _result = result;
305 _controller!.reverse();
306 return super.didPop(result);
307 }
308
309 @override
310 void didPopNext(Route<dynamic> nextRoute) {
311 assert(_controller != null, '$runtimeType.didPopNext called before calling install() or after calling dispose().');
312 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
313 _updateSecondaryAnimation(nextRoute);
314 super.didPopNext(nextRoute);
315 }
316
317 @override
318 void didChangeNext(Route<dynamic>? nextRoute) {
319 assert(_controller != null, '$runtimeType.didChangeNext called before calling install() or after calling dispose().');
320 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
321 _updateSecondaryAnimation(nextRoute);
322 super.didChangeNext(nextRoute);
323 }
324
325 // A callback method that disposes existing train hopping animation and
326 // removes its listener.
327 //
328 // This property is non-null if there is a train hopping in progress, and the
329 // caller must reset this property to null after it is called.
330 VoidCallback? _trainHoppingListenerRemover;
331
332 void _updateSecondaryAnimation(Route<dynamic>? nextRoute) {
333 // There is an existing train hopping in progress. Unfortunately, we cannot
334 // dispose current train hopping animation until we replace it with a new
335 // animation.
336 final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
337 _trainHoppingListenerRemover = null;
338
339 if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
340 final Animation<double>? current = _secondaryAnimation.parent;
341 if (current != null) {
342 final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!;
343 final Animation<double> nextTrain = nextRoute._animation!;
344 if (
345 currentTrain.value == nextTrain.value ||
346 nextTrain.status == AnimationStatus.completed ||
347 nextTrain.status == AnimationStatus.dismissed
348 ) {
349 _setSecondaryAnimation(nextTrain, nextRoute.completed);
350 } else {
351 // Two trains animate at different values. We have to do train hopping.
352 // There are three possibilities of train hopping:
353 // 1. We hop on the nextTrain when two trains meet in the middle using
354 // TrainHoppingAnimation.
355 // 2. There is no chance to hop on nextTrain because two trains never
356 // cross each other. We have to directly set the animation to
357 // nextTrain once the nextTrain stops animating.
358 // 3. A new _updateSecondaryAnimation is called before train hopping
359 // finishes. We leave a listener remover for the next call to
360 // properly clean up the existing train hopping.
361 TrainHoppingAnimation? newAnimation;
362 void jumpOnAnimationEnd(AnimationStatus status) {
363 switch (status) {
364 case AnimationStatus.completed:
365 case AnimationStatus.dismissed:
366 // The nextTrain has stopped animating without train hopping.
367 // Directly sets the secondary animation and disposes the
368 // TrainHoppingAnimation.
369 _setSecondaryAnimation(nextTrain, nextRoute.completed);
370 if (_trainHoppingListenerRemover != null) {
371 _trainHoppingListenerRemover!();
372 _trainHoppingListenerRemover = null;
373 }
374 case AnimationStatus.forward:
375 case AnimationStatus.reverse:
376 break;
377 }
378 }
379 _trainHoppingListenerRemover = () {
380 nextTrain.removeStatusListener(jumpOnAnimationEnd);
381 newAnimation?.dispose();
382 };
383 nextTrain.addStatusListener(jumpOnAnimationEnd);
384 newAnimation = TrainHoppingAnimation(
385 currentTrain,
386 nextTrain,
387 onSwitchedTrain: () {
388 assert(_secondaryAnimation.parent == newAnimation);
389 assert(newAnimation!.currentTrain == nextRoute._animation);
390 // We can hop on the nextTrain, so we don't need to listen to
391 // whether the nextTrain has stopped.
392 _setSecondaryAnimation(newAnimation!.currentTrain, nextRoute.completed);
393 if (_trainHoppingListenerRemover != null) {
394 _trainHoppingListenerRemover!();
395 _trainHoppingListenerRemover = null;
396 }
397 },
398 );
399 _setSecondaryAnimation(newAnimation, nextRoute.completed);
400 }
401 } else {
402 _setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
403 }
404 } else {
405 _setSecondaryAnimation(kAlwaysDismissedAnimation);
406 }
407 // Finally, we dispose any previous train hopping animation because it
408 // has been successfully updated at this point.
409 if (previousTrainHoppingListenerRemover != null) {
410 previousTrainHoppingListenerRemover();
411 }
412 }
413
414 void _setSecondaryAnimation(Animation<double>? animation, [Future<dynamic>? disposed]) {
415 _secondaryAnimation.parent = animation;
416 // Releases the reference to the next route's animation when that route
417 // is disposed.
418 disposed?.then((dynamic _) {
419 if (_secondaryAnimation.parent == animation) {
420 _secondaryAnimation.parent = kAlwaysDismissedAnimation;
421 if (animation is TrainHoppingAnimation) {
422 animation.dispose();
423 }
424 }
425 });
426 }
427
428 /// Returns true if this route supports a transition animation that runs
429 /// when [nextRoute] is pushed on top of it or when [nextRoute] is popped
430 /// off of it.
431 ///
432 /// Subclasses can override this method to restrict the set of routes they
433 /// need to coordinate transitions with.
434 ///
435 /// If true, and `nextRoute.canTransitionFrom()` is true, then the
436 /// [ModalRoute.buildTransitions] `secondaryAnimation` will run from 0.0 - 1.0
437 /// when [nextRoute] is pushed on top of this one. Similarly, if
438 /// the [nextRoute] is popped off of this route, the
439 /// `secondaryAnimation` will run from 1.0 - 0.0.
440 ///
441 /// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation` parameter
442 /// value will be [kAlwaysDismissedAnimation]. In other words, this route
443 /// will not animate when [nextRoute] is pushed on top of it or when
444 /// [nextRoute] is popped off of it.
445 ///
446 /// Returns true by default.
447 ///
448 /// See also:
449 ///
450 /// * [canTransitionFrom], which must be true for [nextRoute] for the
451 /// [ModalRoute.buildTransitions] `secondaryAnimation` to run.
452 bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
453
454 /// Returns true if [previousRoute] should animate when this route
455 /// is pushed on top of it or when then this route is popped off of it.
456 ///
457 /// Subclasses can override this method to restrict the set of routes they
458 /// need to coordinate transitions with.
459 ///
460 /// If true, and `previousRoute.canTransitionTo()` is true, then the
461 /// previous route's [ModalRoute.buildTransitions] `secondaryAnimation` will
462 /// run from 0.0 - 1.0 when this route is pushed on top of
463 /// it. Similarly, if this route is popped off of [previousRoute]
464 /// the previous route's `secondaryAnimation` will run from 1.0 - 0.0.
465 ///
466 /// If false, then the previous route's [ModalRoute.buildTransitions]
467 /// `secondaryAnimation` value will be kAlwaysDismissedAnimation. In
468 /// other words [previousRoute] will not animate when this route is
469 /// pushed on top of it or when then this route is popped off of it.
470 ///
471 /// Returns true by default.
472 ///
473 /// See also:
474 ///
475 /// * [canTransitionTo], which must be true for [previousRoute] for its
476 /// [ModalRoute.buildTransitions] `secondaryAnimation` to run.
477 bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true;
478
479 @override
480 void dispose() {
481 assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
482 _animation?.removeStatusListener(_handleStatusChanged);
483 _performanceModeRequestHandle?.dispose();
484 _performanceModeRequestHandle = null;
485 if (willDisposeAnimationController) {
486 _controller?.dispose();
487 }
488 _transitionCompleter.complete(_result);
489 super.dispose();
490 }
491
492 /// A short description of this route useful for debugging.
493 String get debugLabel => objectRuntimeType(this, 'TransitionRoute');
494
495 @override
496 String toString() => '${objectRuntimeType(this, 'TransitionRoute')}(animation: $_controller)';
497}
498
499/// An entry in the history of a [LocalHistoryRoute].
500class LocalHistoryEntry {
501 /// Creates an entry in the history of a [LocalHistoryRoute].
502 ///
503 /// The [impliesAppBarDismissal] defaults to true if not provided.
504 LocalHistoryEntry({ this.onRemove, this.impliesAppBarDismissal = true });
505
506 /// Called when this entry is removed from the history of its associated [LocalHistoryRoute].
507 final VoidCallback? onRemove;
508
509 LocalHistoryRoute<dynamic>? _owner;
510
511 /// Whether an [AppBar] in the route this entry belongs to should
512 /// automatically add a back button or close button.
513 ///
514 /// Defaults to true.
515 final bool impliesAppBarDismissal;
516
517 /// Remove this entry from the history of its associated [LocalHistoryRoute].
518 void remove() {
519 _owner?.removeLocalHistoryEntry(this);
520 assert(_owner == null);
521 }
522
523 void _notifyRemoved() {
524 onRemove?.call();
525 }
526}
527
528/// A mixin used by routes to handle back navigations internally by popping a list.
529///
530/// When a [Navigator] is instructed to pop, the current route is given an
531/// opportunity to handle the pop internally. A [LocalHistoryRoute] handles the
532/// pop internally if its list of local history entries is non-empty. Rather
533/// than being removed as the current route, the most recent [LocalHistoryEntry]
534/// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
535///
536/// See also:
537///
538/// * [Route], which documents the meaning of the `T` generic type argument.
539mixin LocalHistoryRoute<T> on Route<T> {
540 List<LocalHistoryEntry>? _localHistory;
541 int _entriesImpliesAppBarDismissal = 0;
542 /// Adds a local history entry to this route.
543 ///
544 /// When asked to pop, if this route has any local history entries, this route
545 /// will handle the pop internally by removing the most recently added local
546 /// history entry.
547 ///
548 /// The given local history entry must not already be part of another local
549 /// history route.
550 ///
551 /// {@tool snippet}
552 ///
553 /// The following example is an app with 2 pages: `HomePage` and `SecondPage`.
554 /// The `HomePage` can navigate to the `SecondPage`.
555 ///
556 /// The `SecondPage` uses a [LocalHistoryEntry] to implement local navigation
557 /// within that page. Pressing 'show rectangle' displays a red rectangle and
558 /// adds a local history entry. At that point, pressing the '< back' button
559 /// pops the latest route, which is the local history entry, and the red
560 /// rectangle disappears. Pressing the '< back' button a second time
561 /// once again pops the latest route, which is the `SecondPage`, itself.
562 /// Therefore, the second press navigates back to the `HomePage`.
563 ///
564 /// ```dart
565 /// class App extends StatelessWidget {
566 /// const App({super.key});
567 ///
568 /// @override
569 /// Widget build(BuildContext context) {
570 /// return MaterialApp(
571 /// initialRoute: '/',
572 /// routes: <String, WidgetBuilder>{
573 /// '/': (BuildContext context) => const HomePage(),
574 /// '/second_page': (BuildContext context) => const SecondPage(),
575 /// },
576 /// );
577 /// }
578 /// }
579 ///
580 /// class HomePage extends StatefulWidget {
581 /// const HomePage({super.key});
582 ///
583 /// @override
584 /// State<HomePage> createState() => _HomePageState();
585 /// }
586 ///
587 /// class _HomePageState extends State<HomePage> {
588 /// @override
589 /// Widget build(BuildContext context) {
590 /// return Scaffold(
591 /// body: Center(
592 /// child: Column(
593 /// mainAxisSize: MainAxisSize.min,
594 /// children: <Widget>[
595 /// const Text('HomePage'),
596 /// // Press this button to open the SecondPage.
597 /// ElevatedButton(
598 /// child: const Text('Second Page >'),
599 /// onPressed: () {
600 /// Navigator.pushNamed(context, '/second_page');
601 /// },
602 /// ),
603 /// ],
604 /// ),
605 /// ),
606 /// );
607 /// }
608 /// }
609 ///
610 /// class SecondPage extends StatefulWidget {
611 /// const SecondPage({super.key});
612 ///
613 /// @override
614 /// State<SecondPage> createState() => _SecondPageState();
615 /// }
616 ///
617 /// class _SecondPageState extends State<SecondPage> {
618 ///
619 /// bool _showRectangle = false;
620 ///
621 /// Future<void> _navigateLocallyToShowRectangle() async {
622 /// // This local history entry essentially represents the display of the red
623 /// // rectangle. When this local history entry is removed, we hide the red
624 /// // rectangle.
625 /// setState(() => _showRectangle = true);
626 /// ModalRoute.of(context)?.addLocalHistoryEntry(
627 /// LocalHistoryEntry(
628 /// onRemove: () {
629 /// // Hide the red rectangle.
630 /// setState(() => _showRectangle = false);
631 /// }
632 /// )
633 /// );
634 /// }
635 ///
636 /// @override
637 /// Widget build(BuildContext context) {
638 /// final Widget localNavContent = _showRectangle
639 /// ? Container(
640 /// width: 100.0,
641 /// height: 100.0,
642 /// color: Colors.red,
643 /// )
644 /// : ElevatedButton(
645 /// onPressed: _navigateLocallyToShowRectangle,
646 /// child: const Text('Show Rectangle'),
647 /// );
648 ///
649 /// return Scaffold(
650 /// body: Center(
651 /// child: Column(
652 /// mainAxisAlignment: MainAxisAlignment.center,
653 /// children: <Widget>[
654 /// localNavContent,
655 /// ElevatedButton(
656 /// child: const Text('< Back'),
657 /// onPressed: () {
658 /// // Pop a route. If this is pressed while the red rectangle is
659 /// // visible then it will pop our local history entry, which
660 /// // will hide the red rectangle. Otherwise, the SecondPage will
661 /// // navigate back to the HomePage.
662 /// Navigator.of(context).pop();
663 /// },
664 /// ),
665 /// ],
666 /// ),
667 /// ),
668 /// );
669 /// }
670 /// }
671 /// ```
672 /// {@end-tool}
673 void addLocalHistoryEntry(LocalHistoryEntry entry) {
674 assert(entry._owner == null);
675 entry._owner = this;
676 _localHistory ??= <LocalHistoryEntry>[];
677 final bool wasEmpty = _localHistory!.isEmpty;
678 _localHistory!.add(entry);
679 bool internalStateChanged = false;
680 if (entry.impliesAppBarDismissal) {
681 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
682 _entriesImpliesAppBarDismissal += 1;
683 }
684 if (wasEmpty || internalStateChanged) {
685 changedInternalState();
686 }
687 }
688
689 /// Remove a local history entry from this route.
690 ///
691 /// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called
692 /// synchronously.
693 void removeLocalHistoryEntry(LocalHistoryEntry entry) {
694 assert(entry._owner == this);
695 assert(_localHistory!.contains(entry));
696 bool internalStateChanged = false;
697 if (_localHistory!.remove(entry) && entry.impliesAppBarDismissal) {
698 _entriesImpliesAppBarDismissal -= 1;
699 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
700 }
701 entry._owner = null;
702 entry._notifyRemoved();
703 if (_localHistory!.isEmpty || internalStateChanged) {
704 assert(_entriesImpliesAppBarDismissal == 0);
705 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
706 // The local history might be removed as a result of disposing inactive
707 // elements during finalizeTree. The state is locked at this moment, and
708 // we can only notify state has changed in the next frame.
709 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
710 if (isActive) {
711 changedInternalState();
712 }
713 }, debugLabel: 'LocalHistoryRoute.changedInternalState');
714 } else {
715 changedInternalState();
716 }
717 }
718 }
719
720 @Deprecated(
721 'Use popDisposition instead. '
722 'This feature was deprecated after v3.12.0-1.0.pre.',
723 )
724 @override
725 Future<RoutePopDisposition> willPop() async {
726 if (willHandlePopInternally) {
727 return RoutePopDisposition.pop;
728 }
729 return super.willPop();
730 }
731
732 @override
733 RoutePopDisposition get popDisposition {
734 if (willHandlePopInternally) {
735 return RoutePopDisposition.pop;
736 }
737 return super.popDisposition;
738 }
739
740 @override
741 bool didPop(T? result) {
742 if (_localHistory != null && _localHistory!.isNotEmpty) {
743 final LocalHistoryEntry entry = _localHistory!.removeLast();
744 assert(entry._owner == this);
745 entry._owner = null;
746 entry._notifyRemoved();
747 bool internalStateChanged = false;
748 if (entry.impliesAppBarDismissal) {
749 _entriesImpliesAppBarDismissal -= 1;
750 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
751 }
752 if (_localHistory!.isEmpty || internalStateChanged) {
753 changedInternalState();
754 }
755 return false;
756 }
757 return super.didPop(result);
758 }
759
760 @override
761 bool get willHandlePopInternally {
762 return _localHistory != null && _localHistory!.isNotEmpty;
763 }
764}
765
766class _DismissModalAction extends DismissAction {
767 _DismissModalAction(this.context);
768
769 final BuildContext context;
770
771 @override
772 bool isEnabled(DismissIntent intent) {
773 final ModalRoute<dynamic> route = ModalRoute.of<dynamic>(context)!;
774 return route.barrierDismissible;
775 }
776
777 @override
778 Object invoke(DismissIntent intent) {
779 return Navigator.of(context).maybePop();
780 }
781}
782
783class _ModalScopeStatus extends InheritedWidget {
784 const _ModalScopeStatus({
785 required this.isCurrent,
786 required this.canPop,
787 required this.impliesAppBarDismissal,
788 required this.route,
789 required super.child,
790 });
791
792 final bool isCurrent;
793 final bool canPop;
794 final bool impliesAppBarDismissal;
795 final Route<dynamic> route;
796
797 @override
798 bool updateShouldNotify(_ModalScopeStatus old) {
799 return isCurrent != old.isCurrent ||
800 canPop != old.canPop ||
801 impliesAppBarDismissal != old.impliesAppBarDismissal ||
802 route != old.route;
803 }
804
805 @override
806 void debugFillProperties(DiagnosticPropertiesBuilder description) {
807 super.debugFillProperties(description);
808 description.add(FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive'));
809 description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
810 description.add(FlagProperty('impliesAppBarDismissal', value: impliesAppBarDismissal, ifTrue: 'implies app bar dismissal'));
811 }
812}
813
814class _ModalScope<T> extends StatefulWidget {
815 const _ModalScope({
816 super.key,
817 required this.route,
818 });
819
820 final ModalRoute<T> route;
821
822 @override
823 _ModalScopeState<T> createState() => _ModalScopeState<T>();
824}
825
826class _ModalScopeState<T> extends State<_ModalScope<T>> {
827 // We cache the result of calling the route's buildPage, and clear the cache
828 // whenever the dependencies change. This implements the contract described in
829 // the documentation for buildPage, namely that it gets called once, unless
830 // something like a ModalRoute.of() dependency triggers an update.
831 Widget? _page;
832
833 // This is the combination of the two animations for the route.
834 late Listenable _listenable;
835
836 /// The node this scope will use for its root [FocusScope] widget.
837 final FocusScopeNode focusScopeNode = FocusScopeNode(
838 debugLabel: '$_ModalScopeState Focus Scope',
839 );
840 final ScrollController primaryScrollController = ScrollController();
841
842 @override
843 void initState() {
844 super.initState();
845 final List<Listenable> animations = <Listenable>[
846 if (widget.route.animation != null) widget.route.animation!,
847 if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!,
848 ];
849 _listenable = Listenable.merge(animations);
850 }
851
852 @override
853 void didUpdateWidget(_ModalScope<T> oldWidget) {
854 super.didUpdateWidget(oldWidget);
855 assert(widget.route == oldWidget.route);
856 _updateFocusScopeNode();
857 }
858
859 @override
860 void didChangeDependencies() {
861 super.didChangeDependencies();
862 _page = null;
863 _updateFocusScopeNode();
864 }
865
866 void _updateFocusScopeNode() {
867 final TraversalEdgeBehavior traversalEdgeBehavior;
868 final ModalRoute<T> route = widget.route;
869 if (route.traversalEdgeBehavior != null) {
870 traversalEdgeBehavior = route.traversalEdgeBehavior!;
871 } else {
872 traversalEdgeBehavior = route.navigator!.widget.routeTraversalEdgeBehavior;
873 }
874 focusScopeNode.traversalEdgeBehavior = traversalEdgeBehavior;
875 if (route.isCurrent && _shouldRequestFocus) {
876 route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
877 }
878 }
879
880 void _forceRebuildPage() {
881 setState(() {
882 _page = null;
883 });
884 }
885
886 @override
887 void dispose() {
888 focusScopeNode.dispose();
889 primaryScrollController.dispose();
890 super.dispose();
891 }
892
893 bool get _shouldIgnoreFocusRequest {
894 return widget.route.animation?.status == AnimationStatus.reverse ||
895 (widget.route.navigator?.userGestureInProgress ?? false);
896 }
897
898 bool get _shouldRequestFocus {
899 return widget.route.navigator!.widget.requestFocus;
900 }
901
902 // This should be called to wrap any changes to route.isCurrent, route.canPop,
903 // and route.offstage.
904 void _routeSetState(VoidCallback fn) {
905 if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
906 widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
907 }
908 setState(fn);
909 }
910
911 @override
912 Widget build(BuildContext context) {
913 return AnimatedBuilder(
914 animation: widget.route.restorationScopeId,
915 builder: (BuildContext context, Widget? child) {
916 assert(child != null);
917 return RestorationScope(
918 restorationId: widget.route.restorationScopeId.value,
919 child: child!,
920 );
921 },
922 child: _ModalScopeStatus(
923 route: widget.route,
924 isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
925 canPop: widget.route.canPop, // _routeSetState is called if this updates
926 impliesAppBarDismissal: widget.route.impliesAppBarDismissal,
927 child: Offstage(
928 offstage: widget.route.offstage, // _routeSetState is called if this updates
929 child: PageStorage(
930 bucket: widget.route._storageBucket, // immutable
931 child: Builder(
932 builder: (BuildContext context) {
933 return Actions(
934 actions: <Type, Action<Intent>>{
935 DismissIntent: _DismissModalAction(context),
936 },
937 child: PrimaryScrollController(
938 controller: primaryScrollController,
939 child: FocusScope(
940 node: focusScopeNode, // immutable
941 // Only top most route can participate in focus traversal.
942 skipTraversal: !widget.route.isCurrent,
943 child: RepaintBoundary(
944 child: AnimatedBuilder(
945 animation: _listenable, // immutable
946 builder: (BuildContext context, Widget? child) {
947 return widget.route.buildTransitions(
948 context,
949 widget.route.animation!,
950 widget.route.secondaryAnimation!,
951 // This additional AnimatedBuilder is include because if the
952 // value of the userGestureInProgressNotifier changes, it's
953 // only necessary to rebuild the IgnorePointer widget and set
954 // the focus node's ability to focus.
955 AnimatedBuilder(
956 animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),
957 builder: (BuildContext context, Widget? child) {
958 final bool ignoreEvents = _shouldIgnoreFocusRequest;
959 focusScopeNode.canRequestFocus = !ignoreEvents;
960 return IgnorePointer(
961 ignoring: ignoreEvents,
962 child: child,
963 );
964 },
965 child: child,
966 ),
967 );
968 },
969 child: _page ??= RepaintBoundary(
970 key: widget.route._subtreeKey, // immutable
971 child: Builder(
972 builder: (BuildContext context) {
973 return widget.route.buildPage(
974 context,
975 widget.route.animation!,
976 widget.route.secondaryAnimation!,
977 );
978 },
979 ),
980 ),
981 ),
982 ),
983 ),
984 ),
985 );
986 },
987 ),
988 ),
989 ),
990 ),
991 );
992 }
993}
994
995/// A route that blocks interaction with previous routes.
996///
997/// [ModalRoute]s cover the entire [Navigator]. They are not necessarily
998/// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only
999/// shows the menu in a small box overlapping the previous route.
1000///
1001/// The `T` type argument is the return value of the route. If there is no
1002/// return value, consider using `void` as the return value.
1003///
1004/// See also:
1005///
1006/// * [Route], which further documents the meaning of the `T` generic type argument.
1007abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
1008 /// Creates a route that blocks interaction with previous routes.
1009 ModalRoute({
1010 super.settings,
1011 this.filter,
1012 this.traversalEdgeBehavior,
1013 });
1014
1015 /// The filter to add to the barrier.
1016 ///
1017 /// If given, this filter will be applied to the modal barrier using
1018 /// [BackdropFilter]. This allows blur effects, for example.
1019 final ui.ImageFilter? filter;
1020
1021 /// Controls the transfer of focus beyond the first and the last items of a
1022 /// [FocusScopeNode].
1023 ///
1024 /// If set to null, [Navigator.routeTraversalEdgeBehavior] is used.
1025 final TraversalEdgeBehavior? traversalEdgeBehavior;
1026
1027 // The API for general users of this class
1028
1029 /// Returns the modal route most closely associated with the given context.
1030 ///
1031 /// Returns null if the given context is not associated with a modal route.
1032 ///
1033 /// {@tool snippet}
1034 ///
1035 /// Typical usage is as follows:
1036 ///
1037 /// ```dart
1038 /// ModalRoute<int>? route = ModalRoute.of<int>(context);
1039 /// ```
1040 /// {@end-tool}
1041 ///
1042 /// The given [BuildContext] will be rebuilt if the state of the route changes
1043 /// while it is visible (specifically, if [isCurrent] or [canPop] change value).
1044 @optionalTypeArgs
1045 static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
1046 final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
1047 return widget?.route as ModalRoute<T>?;
1048 }
1049
1050 /// Schedule a call to [buildTransitions].
1051 ///
1052 /// Whenever you need to change internal state for a [ModalRoute] object, make
1053 /// the change in a function that you pass to [setState], as in:
1054 ///
1055 /// ```dart
1056 /// setState(() { _myState = newValue; });
1057 /// ```
1058 ///
1059 /// If you just change the state directly without calling [setState], then the
1060 /// route will not be scheduled for rebuilding, meaning that its rendering
1061 /// will not be updated.
1062 @protected
1063 void setState(VoidCallback fn) {
1064 if (_scopeKey.currentState != null) {
1065 _scopeKey.currentState!._routeSetState(fn);
1066 } else {
1067 // The route isn't currently visible, so we don't have to call its setState
1068 // method, but we do still need to call the fn callback, otherwise the state
1069 // in the route won't be updated!
1070 fn();
1071 }
1072 }
1073
1074 /// Returns a predicate that's true if the route has the specified name and if
1075 /// popping the route will not yield the same route, i.e. if the route's
1076 /// [willHandlePopInternally] property is false.
1077 ///
1078 /// This function is typically used with [Navigator.popUntil()].
1079 static RoutePredicate withName(String name) {
1080 return (Route<dynamic> route) {
1081 return !route.willHandlePopInternally
1082 && route is ModalRoute
1083 && route.settings.name == name;
1084 };
1085 }
1086
1087 // The API for subclasses to override - used by _ModalScope
1088
1089 /// Override this method to build the primary content of this route.
1090 ///
1091 /// The arguments have the following meanings:
1092 ///
1093 /// * `context`: The context in which the route is being built.
1094 /// * [animation]: The animation for this route's transition. When entering,
1095 /// the animation runs forward from 0.0 to 1.0. When exiting, this animation
1096 /// runs backwards from 1.0 to 0.0.
1097 /// * [secondaryAnimation]: The animation for the route being pushed on top of
1098 /// this route. This animation lets this route coordinate with the entrance
1099 /// and exit transition of routes pushed on top of this route.
1100 ///
1101 /// This method is only called when the route is first built, and rarely
1102 /// thereafter. In particular, it is not automatically called again when the
1103 /// route's state changes unless it uses [ModalRoute.of]. For a builder that
1104 /// is called every time the route's state changes, consider
1105 /// [buildTransitions]. For widgets that change their behavior when the
1106 /// route's state changes, consider [ModalRoute.of] to obtain a reference to
1107 /// the route; this will cause the widget to be rebuilt each time the route
1108 /// changes state.
1109 ///
1110 /// In general, [buildPage] should be used to build the page contents, and
1111 /// [buildTransitions] for the widgets that change as the page is brought in
1112 /// and out of view. Avoid using [buildTransitions] for content that never
1113 /// changes; building such content once from [buildPage] is more efficient.
1114 Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
1115
1116 /// Override this method to wrap the [child] with one or more transition
1117 /// widgets that define how the route arrives on and leaves the screen.
1118 ///
1119 /// By default, the child (which contains the widget returned by [buildPage])
1120 /// is not wrapped in any transition widgets.
1121 ///
1122 /// The [buildTransitions] method, in contrast to [buildPage], is called each
1123 /// time the [Route]'s state changes while it is visible (e.g. if the value of
1124 /// [canPop] changes on the active route).
1125 ///
1126 /// The [buildTransitions] method is typically used to define transitions
1127 /// that animate the new topmost route's comings and goings. When the
1128 /// [Navigator] pushes a route on the top of its stack, the new route's
1129 /// primary [animation] runs from 0.0 to 1.0. When the Navigator pops the
1130 /// topmost route, e.g. because the use pressed the back button, the
1131 /// primary animation runs from 1.0 to 0.0.
1132 ///
1133 /// {@tool snippet}
1134 /// The following example uses the primary animation to drive a
1135 /// [SlideTransition] that translates the top of the new route vertically
1136 /// from the bottom of the screen when it is pushed on the Navigator's
1137 /// stack. When the route is popped the SlideTransition translates the
1138 /// route from the top of the screen back to the bottom.
1139 ///
1140 /// We've used [PageRouteBuilder] to demonstrate the [buildTransitions] method
1141 /// here. The body of an override of the [buildTransitions] method would be
1142 /// defined in the same way.
1143 ///
1144 /// ```dart
1145 /// PageRouteBuilder<void>(
1146 /// pageBuilder: (BuildContext context,
1147 /// Animation<double> animation,
1148 /// Animation<double> secondaryAnimation,
1149 /// ) {
1150 /// return Scaffold(
1151 /// appBar: AppBar(title: const Text('Hello')),
1152 /// body: const Center(
1153 /// child: Text('Hello World'),
1154 /// ),
1155 /// );
1156 /// },
1157 /// transitionsBuilder: (
1158 /// BuildContext context,
1159 /// Animation<double> animation,
1160 /// Animation<double> secondaryAnimation,
1161 /// Widget child,
1162 /// ) {
1163 /// return SlideTransition(
1164 /// position: Tween<Offset>(
1165 /// begin: const Offset(0.0, 1.0),
1166 /// end: Offset.zero,
1167 /// ).animate(animation),
1168 /// child: child, // child is the value returned by pageBuilder
1169 /// );
1170 /// },
1171 /// )
1172 /// ```
1173 /// {@end-tool}
1174 ///
1175 /// When the [Navigator] pushes a route on the top of its stack, the
1176 /// [secondaryAnimation] can be used to define how the route that was on
1177 /// the top of the stack leaves the screen. Similarly when the topmost route
1178 /// is popped, the secondaryAnimation can be used to define how the route
1179 /// below it reappears on the screen. When the Navigator pushes a new route
1180 /// on the top of its stack, the old topmost route's secondaryAnimation
1181 /// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
1182 /// secondaryAnimation for the route below it runs from 1.0 to 0.0.
1183 ///
1184 /// {@tool snippet}
1185 /// The example below adds a transition that's driven by the
1186 /// [secondaryAnimation]. When this route disappears because a new route has
1187 /// been pushed on top of it, it translates in the opposite direction of
1188 /// the new route. Likewise when the route is exposed because the topmost
1189 /// route has been popped off.
1190 ///
1191 /// ```dart
1192 /// PageRouteBuilder<void>(
1193 /// pageBuilder: (BuildContext context,
1194 /// Animation<double> animation,
1195 /// Animation<double> secondaryAnimation,
1196 /// ) {
1197 /// return Scaffold(
1198 /// appBar: AppBar(title: const Text('Hello')),
1199 /// body: const Center(
1200 /// child: Text('Hello World'),
1201 /// ),
1202 /// );
1203 /// },
1204 /// transitionsBuilder: (
1205 /// BuildContext context,
1206 /// Animation<double> animation,
1207 /// Animation<double> secondaryAnimation,
1208 /// Widget child,
1209 /// ) {
1210 /// return SlideTransition(
1211 /// position: Tween<Offset>(
1212 /// begin: const Offset(0.0, 1.0),
1213 /// end: Offset.zero,
1214 /// ).animate(animation),
1215 /// child: SlideTransition(
1216 /// position: Tween<Offset>(
1217 /// begin: Offset.zero,
1218 /// end: const Offset(0.0, 1.0),
1219 /// ).animate(secondaryAnimation),
1220 /// child: child,
1221 /// ),
1222 /// );
1223 /// },
1224 /// )
1225 /// ```
1226 /// {@end-tool}
1227 ///
1228 /// In practice the `secondaryAnimation` is used pretty rarely.
1229 ///
1230 /// The arguments to this method are as follows:
1231 ///
1232 /// * `context`: The context in which the route is being built.
1233 /// * [animation]: When the [Navigator] pushes a route on the top of its stack,
1234 /// the new route's primary [animation] runs from 0.0 to 1.0. When the [Navigator]
1235 /// pops the topmost route this animation runs from 1.0 to 0.0.
1236 /// * [secondaryAnimation]: When the Navigator pushes a new route
1237 /// on the top of its stack, the old topmost route's [secondaryAnimation]
1238 /// runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the
1239 /// [secondaryAnimation] for the route below it runs from 1.0 to 0.0.
1240 /// * `child`, the page contents, as returned by [buildPage].
1241 ///
1242 /// See also:
1243 ///
1244 /// * [buildPage], which is used to describe the actual contents of the page,
1245 /// and whose result is passed to the `child` argument of this method.
1246 Widget buildTransitions(
1247 BuildContext context,
1248 Animation<double> animation,
1249 Animation<double> secondaryAnimation,
1250 Widget child,
1251 ) {
1252 return child;
1253 }
1254
1255 @override
1256 void install() {
1257 super.install();
1258 _animationProxy = ProxyAnimation(super.animation);
1259 _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
1260 }
1261
1262 @override
1263 TickerFuture didPush() {
1264 if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
1265 navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
1266 }
1267 return super.didPush();
1268 }
1269
1270 @override
1271 void didAdd() {
1272 if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
1273 navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
1274 }
1275 super.didAdd();
1276 }
1277
1278 // The API for subclasses to override - used by this class
1279
1280 /// {@template flutter.widgets.ModalRoute.barrierDismissible}
1281 /// Whether you can dismiss this route by tapping the modal barrier.
1282 ///
1283 /// The modal barrier is the scrim that is rendered behind each route, which
1284 /// generally prevents the user from interacting with the route below the
1285 /// current route, and normally partially obscures such routes.
1286 ///
1287 /// For example, when a dialog is on the screen, the page below the dialog is
1288 /// usually darkened by the modal barrier.
1289 ///
1290 /// If [barrierDismissible] is true, then tapping this barrier, pressing
1291 /// the escape key on the keyboard, or calling route popping functions
1292 /// such as [Navigator.pop] will cause the current route to be popped
1293 /// with null as the value.
1294 ///
1295 /// If [barrierDismissible] is false, then tapping the barrier has no effect.
1296 ///
1297 /// If this getter would ever start returning a different value,
1298 /// either [changedInternalState] or [changedExternalState] should
1299 /// be invoked so that the change can take effect.
1300 ///
1301 /// It is safe to use `navigator.context` to look up inherited
1302 /// widgets here, because the [Navigator] calls
1303 /// [changedExternalState] whenever its dependencies change, and
1304 /// [changedExternalState] causes the modal barrier to rebuild.
1305 ///
1306 /// See also:
1307 ///
1308 /// * [Navigator.pop], which is used to dismiss the route.
1309 /// * [barrierColor], which controls the color of the scrim for this route.
1310 /// * [ModalBarrier], the widget that implements this feature.
1311 /// {@endtemplate}
1312 bool get barrierDismissible;
1313
1314 /// Whether the semantics of the modal barrier are included in the
1315 /// semantics tree.
1316 ///
1317 /// The modal barrier is the scrim that is rendered behind each route, which
1318 /// generally prevents the user from interacting with the route below the
1319 /// current route, and normally partially obscures such routes.
1320 ///
1321 /// If [semanticsDismissible] is true, then modal barrier semantics are
1322 /// included in the semantics tree.
1323 ///
1324 /// If [semanticsDismissible] is false, then modal barrier semantics are
1325 /// excluded from the semantics tree and tapping on the modal barrier
1326 /// has no effect.
1327 ///
1328 /// If this getter would ever start returning a different value,
1329 /// either [changedInternalState] or [changedExternalState] should
1330 /// be invoked so that the change can take effect.
1331 ///
1332 /// It is safe to use `navigator.context` to look up inherited
1333 /// widgets here, because the [Navigator] calls
1334 /// [changedExternalState] whenever its dependencies change, and
1335 /// [changedExternalState] causes the modal barrier to rebuild.
1336 bool get semanticsDismissible => true;
1337
1338 /// {@template flutter.widgets.ModalRoute.barrierColor}
1339 /// The color to use for the modal barrier. If this is null, the barrier will
1340 /// be transparent.
1341 ///
1342 /// The modal barrier is the scrim that is rendered behind each route, which
1343 /// generally prevents the user from interacting with the route below the
1344 /// current route, and normally partially obscures such routes.
1345 ///
1346 /// For example, when a dialog is on the screen, the page below the dialog is
1347 /// usually darkened by the modal barrier.
1348 ///
1349 /// The color is ignored, and the barrier made invisible, when
1350 /// [ModalRoute.offstage] is true.
1351 ///
1352 /// While the route is animating into position, the color is animated from
1353 /// transparent to the specified color.
1354 /// {@endtemplate}
1355 ///
1356 /// If this getter would ever start returning a different color, one
1357 /// of the [changedInternalState] or [changedExternalState] methods
1358 /// should be invoked so that the change can take effect.
1359 ///
1360 /// It is safe to use `navigator.context` to look up inherited
1361 /// widgets here, because the [Navigator] calls
1362 /// [changedExternalState] whenever its dependencies change, and
1363 /// [changedExternalState] causes the modal barrier to rebuild.
1364 ///
1365 /// {@tool snippet}
1366 ///
1367 /// For example, to make the barrier color use the theme's
1368 /// background color, one could say:
1369 ///
1370 /// ```dart
1371 /// Color get barrierColor => Theme.of(navigator.context).colorScheme.background;
1372 /// ```
1373 ///
1374 /// {@end-tool}
1375 ///
1376 /// See also:
1377 ///
1378 /// * [barrierDismissible], which controls the behavior of the barrier when
1379 /// tapped.
1380 /// * [ModalBarrier], the widget that implements this feature.
1381 Color? get barrierColor;
1382
1383 /// {@template flutter.widgets.ModalRoute.barrierLabel}
1384 /// The semantic label used for a dismissible barrier.
1385 ///
1386 /// If the barrier is dismissible, this label will be read out if
1387 /// accessibility tools (like VoiceOver on iOS) focus on the barrier.
1388 ///
1389 /// The modal barrier is the scrim that is rendered behind each route, which
1390 /// generally prevents the user from interacting with the route below the
1391 /// current route, and normally partially obscures such routes.
1392 ///
1393 /// For example, when a dialog is on the screen, the page below the dialog is
1394 /// usually darkened by the modal barrier.
1395 /// {@endtemplate}
1396 ///
1397 /// If this getter would ever start returning a different label,
1398 /// either [changedInternalState] or [changedExternalState] should
1399 /// be invoked so that the change can take effect.
1400 ///
1401 /// It is safe to use `navigator.context` to look up inherited
1402 /// widgets here, because the [Navigator] calls
1403 /// [changedExternalState] whenever its dependencies change, and
1404 /// [changedExternalState] causes the modal barrier to rebuild.
1405 ///
1406 /// See also:
1407 ///
1408 /// * [barrierDismissible], which controls the behavior of the barrier when
1409 /// tapped.
1410 /// * [ModalBarrier], the widget that implements this feature.
1411 String? get barrierLabel;
1412
1413 /// The curve that is used for animating the modal barrier in and out.
1414 ///
1415 /// The modal barrier is the scrim that is rendered behind each route, which
1416 /// generally prevents the user from interacting with the route below the
1417 /// current route, and normally partially obscures such routes.
1418 ///
1419 /// For example, when a dialog is on the screen, the page below the dialog is
1420 /// usually darkened by the modal barrier.
1421 ///
1422 /// While the route is animating into position, the color is animated from
1423 /// transparent to the specified [barrierColor].
1424 ///
1425 /// If this getter would ever start returning a different curve,
1426 /// either [changedInternalState] or [changedExternalState] should
1427 /// be invoked so that the change can take effect.
1428 ///
1429 /// It is safe to use `navigator.context` to look up inherited
1430 /// widgets here, because the [Navigator] calls
1431 /// [changedExternalState] whenever its dependencies change, and
1432 /// [changedExternalState] causes the modal barrier to rebuild.
1433 ///
1434 /// It defaults to [Curves.ease].
1435 ///
1436 /// See also:
1437 ///
1438 /// * [barrierColor], which determines the color that the modal transitions
1439 /// to.
1440 /// * [Curves] for a collection of common curves.
1441 /// * [AnimatedModalBarrier], the widget that implements this feature.
1442 Curve get barrierCurve => Curves.ease;
1443
1444 /// {@template flutter.widgets.ModalRoute.maintainState}
1445 /// Whether the route should remain in memory when it is inactive.
1446 ///
1447 /// If this is true, then the route is maintained, so that any futures it is
1448 /// holding from the next route will properly resolve when the next route
1449 /// pops. If this is not necessary, this can be set to false to allow the
1450 /// framework to entirely discard the route's widget hierarchy when it is not
1451 /// visible.
1452 ///
1453 /// Setting [maintainState] to false does not guarantee that the route will be
1454 /// discarded. For instance, it will not be descarded if it is still visible
1455 /// because the next above it is not opaque (e.g. it is a popup dialog).
1456 /// {@endtemplate}
1457 ///
1458 /// If this getter would ever start returning a different value, the
1459 /// [changedInternalState] should be invoked so that the change can take
1460 /// effect.
1461 ///
1462 /// See also:
1463 ///
1464 /// * [OverlayEntry.maintainState], which is the underlying implementation
1465 /// of this property.
1466 bool get maintainState;
1467
1468
1469 // The API for _ModalScope and HeroController
1470
1471 /// Whether this route is currently offstage.
1472 ///
1473 /// On the first frame of a route's entrance transition, the route is built
1474 /// [Offstage] using an animation progress of 1.0. The route is invisible and
1475 /// non-interactive, but each widget has its final size and position. This
1476 /// mechanism lets the [HeroController] determine the final local of any hero
1477 /// widgets being animated as part of the transition.
1478 ///
1479 /// The modal barrier, if any, is not rendered if [offstage] is true (see
1480 /// [barrierColor]).
1481 ///
1482 /// Whenever this changes value, [changedInternalState] is called.
1483 bool get offstage => _offstage;
1484 bool _offstage = false;
1485 set offstage(bool value) {
1486 if (_offstage == value) {
1487 return;
1488 }
1489 setState(() {
1490 _offstage = value;
1491 });
1492 _animationProxy!.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
1493 _secondaryAnimationProxy!.parent = _offstage ? kAlwaysDismissedAnimation : super.secondaryAnimation;
1494 changedInternalState();
1495 }
1496
1497 /// The build context for the subtree containing the primary content of this route.
1498 BuildContext? get subtreeContext => _subtreeKey.currentContext;
1499
1500 @override
1501 Animation<double>? get animation => _animationProxy;
1502 ProxyAnimation? _animationProxy;
1503
1504 @override
1505 Animation<double>? get secondaryAnimation => _secondaryAnimationProxy;
1506 ProxyAnimation? _secondaryAnimationProxy;
1507
1508 final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
1509
1510 final Set<PopEntry> _popEntries = <PopEntry>{};
1511
1512 /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
1513 /// [addScopedWillPopCallback] returns either false or null. If they all
1514 /// return true, the base [Route.willPop]'s result will be returned. The
1515 /// callbacks will be called in the order they were added, and will only be
1516 /// called if all previous callbacks returned true.
1517 ///
1518 /// Typically this method is not overridden because applications usually
1519 /// don't create modal routes directly, they use higher level primitives
1520 /// like [showDialog]. The scoped [WillPopCallback] list makes it possible
1521 /// for ModalRoute descendants to collectively define the value of [willPop].
1522 ///
1523 /// See also:
1524 ///
1525 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
1526 /// * [addScopedWillPopCallback], which adds a callback to the list this
1527 /// method checks.
1528 /// * [removeScopedWillPopCallback], which removes a callback from the list
1529 /// this method checks.
1530 @Deprecated(
1531 'Use popDisposition instead. '
1532 'This feature was deprecated after v3.12.0-1.0.pre.',
1533 )
1534 @override
1535 Future<RoutePopDisposition> willPop() async {
1536 final _ModalScopeState<T>? scope = _scopeKey.currentState;
1537 assert(scope != null);
1538 for (final WillPopCallback callback in List<WillPopCallback>.of(_willPopCallbacks)) {
1539 if (!await callback()) {
1540 return RoutePopDisposition.doNotPop;
1541 }
1542 }
1543 return super.willPop();
1544 }
1545
1546 /// Returns [RoutePopDisposition.doNotPop] if any of the [PopEntry] instances
1547 /// registered with [registerPopEntry] have [PopEntry.canPopNotifier] set to
1548 /// false.
1549 ///
1550 /// Typically this method is not overridden because applications usually
1551 /// don't create modal routes directly, they use higher level primitives
1552 /// like [showDialog]. The scoped [PopEntry] list makes it possible for
1553 /// ModalRoute descendants to collectively define the value of
1554 /// [popDisposition].
1555 ///
1556 /// See also:
1557 ///
1558 /// * [Form], which provides an `onPopInvoked` callback that is similar.
1559 /// * [registerPopEntry], which adds a [PopEntry] to the list this method
1560 /// checks.
1561 /// * [unregisterPopEntry], which removes a [PopEntry] from the list this
1562 /// method checks.
1563 @override
1564 RoutePopDisposition get popDisposition {
1565 final bool canPop = _popEntries.every((PopEntry popEntry) {
1566 return popEntry.canPopNotifier.value;
1567 });
1568
1569 if (!canPop) {
1570 return RoutePopDisposition.doNotPop;
1571 }
1572 return super.popDisposition;
1573 }
1574
1575 @override
1576 void onPopInvoked(bool didPop) {
1577 for (final PopEntry popEntry in _popEntries) {
1578 popEntry.onPopInvoked?.call(didPop);
1579 }
1580 }
1581
1582 /// Enables this route to veto attempts by the user to dismiss it.
1583 ///
1584 /// This callback runs asynchronously and it's possible that it will be called
1585 /// after its route has been disposed. The callback should check [State.mounted]
1586 /// before doing anything.
1587 ///
1588 /// A typical application of this callback would be to warn the user about
1589 /// unsaved [Form] data if the user attempts to back out of the form. In that
1590 /// case, use the [Form.onWillPop] property to register the callback.
1591 ///
1592 /// See also:
1593 ///
1594 /// * [WillPopScope], which manages the registration and unregistration
1595 /// process automatically.
1596 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
1597 /// * [willPop], which runs the callbacks added with this method.
1598 /// * [removeScopedWillPopCallback], which removes a callback from the list
1599 /// that [willPop] checks.
1600 @Deprecated(
1601 'Use registerPopEntry or PopScope instead. '
1602 'This feature was deprecated after v3.12.0-1.0.pre.',
1603 )
1604 void addScopedWillPopCallback(WillPopCallback callback) {
1605 assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
1606 _willPopCallbacks.add(callback);
1607 }
1608
1609 /// Remove one of the callbacks run by [willPop].
1610 ///
1611 /// See also:
1612 ///
1613 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
1614 /// * [addScopedWillPopCallback], which adds callback to the list
1615 /// checked by [willPop].
1616 @Deprecated(
1617 'Use unregisterPopEntry or PopScope instead. '
1618 'This feature was deprecated after v3.12.0-1.0.pre.',
1619 )
1620 void removeScopedWillPopCallback(WillPopCallback callback) {
1621 assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
1622 _willPopCallbacks.remove(callback);
1623 }
1624
1625 /// Registers the existence of a [PopEntry] in the route.
1626 ///
1627 /// [PopEntry] instances registered in this way will have their
1628 /// [PopEntry.onPopInvoked] callbacks called when a route is popped or a pop
1629 /// is attempted. They will also be able to block pop operations with
1630 /// [PopEntry.canPopNotifier] through this route's [popDisposition] method.
1631 ///
1632 /// See also:
1633 ///
1634 /// * [unregisterPopEntry], which performs the opposite operation.
1635 void registerPopEntry(PopEntry popEntry) {
1636 _popEntries.add(popEntry);
1637 popEntry.canPopNotifier.addListener(_handlePopEntryChange);
1638 _handlePopEntryChange();
1639 }
1640
1641 /// Unregisters a [PopEntry] in the route's widget subtree.
1642 ///
1643 /// See also:
1644 ///
1645 /// * [registerPopEntry], which performs the opposite operation.
1646 void unregisterPopEntry(PopEntry popEntry) {
1647 _popEntries.remove(popEntry);
1648 popEntry.canPopNotifier.removeListener(_handlePopEntryChange);
1649 _handlePopEntryChange();
1650 }
1651
1652 void _handlePopEntryChange() {
1653 if (!isCurrent) {
1654 return;
1655 }
1656 final NavigationNotification notification = NavigationNotification(
1657 // canPop indicates that the originator of the Notification can handle a
1658 // pop. In the case of PopScope, it handles pops when canPop is
1659 // false. Hence the seemingly backward logic here.
1660 canHandlePop: popDisposition == RoutePopDisposition.doNotPop,
1661 );
1662 // Avoid dispatching a notification in the middle of a build.
1663 switch (SchedulerBinding.instance.schedulerPhase) {
1664 case SchedulerPhase.postFrameCallbacks:
1665 notification.dispatch(subtreeContext);
1666 case SchedulerPhase.idle:
1667 case SchedulerPhase.midFrameMicrotasks:
1668 case SchedulerPhase.persistentCallbacks:
1669 case SchedulerPhase.transientCallbacks:
1670 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1671 if (!(subtreeContext?.mounted ?? false)) {
1672 return;
1673 }
1674 notification.dispatch(subtreeContext);
1675 }, debugLabel: 'ModalRoute.dispatchNotification');
1676 }
1677 }
1678
1679 /// True if one or more [WillPopCallback] callbacks exist.
1680 ///
1681 /// This method is used to disable the horizontal swipe pop gesture supported
1682 /// by [MaterialPageRoute] for [TargetPlatform.iOS] and
1683 /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
1684 /// disabled.
1685 ///
1686 /// The [buildTransitions] method will not be called again if this changes,
1687 /// since it can change during the build as descendants of the route add or
1688 /// remove callbacks.
1689 ///
1690 /// See also:
1691 ///
1692 /// * [addScopedWillPopCallback], which adds a callback.
1693 /// * [removeScopedWillPopCallback], which removes a callback.
1694 /// * [willHandlePopInternally], which reports on another reason why
1695 /// a pop might be vetoed.
1696 @Deprecated(
1697 'Use popDisposition instead. '
1698 'This feature was deprecated after v3.12.0-1.0.pre.',
1699 )
1700 @protected
1701 bool get hasScopedWillPopCallback {
1702 return _willPopCallbacks.isNotEmpty;
1703 }
1704
1705 @override
1706 void didChangePrevious(Route<dynamic>? previousRoute) {
1707 super.didChangePrevious(previousRoute);
1708 changedInternalState();
1709 }
1710
1711 @override
1712 void didChangeNext(Route<dynamic>? nextRoute) {
1713 super.didChangeNext(nextRoute);
1714 changedInternalState();
1715 }
1716
1717 @override
1718 void didPopNext(Route<dynamic> nextRoute) {
1719 super.didPopNext(nextRoute);
1720 changedInternalState();
1721 }
1722
1723 @override
1724 void changedInternalState() {
1725 super.changedInternalState();
1726 // No need to mark dirty if this method is called during build phase.
1727 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
1728 setState(() { /* internal state already changed */ });
1729 _modalBarrier.markNeedsBuild();
1730 }
1731 _modalScope.maintainState = maintainState;
1732 }
1733
1734 @override
1735 void changedExternalState() {
1736 super.changedExternalState();
1737 _modalBarrier.markNeedsBuild();
1738 if (_scopeKey.currentState != null) {
1739 _scopeKey.currentState!._forceRebuildPage();
1740 }
1741 }
1742
1743 /// Whether this route can be popped.
1744 ///
1745 /// A route can be popped if there is at least one active route below it, or
1746 /// if [willHandlePopInternally] returns true.
1747 ///
1748 /// When this changes, if the route is visible, the route will
1749 /// rebuild, and any widgets that used [ModalRoute.of] will be
1750 /// notified.
1751 bool get canPop => hasActiveRouteBelow || willHandlePopInternally;
1752
1753 /// Whether an [AppBar] in the route should automatically add a back button or
1754 /// close button.
1755 ///
1756 /// This getter returns true if there is at least one active route below it,
1757 /// or there is at least one [LocalHistoryEntry] with [impliesAppBarDismissal]
1758 /// set to true
1759 bool get impliesAppBarDismissal => hasActiveRouteBelow || _entriesImpliesAppBarDismissal > 0;
1760
1761 // Internals
1762
1763 final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>();
1764 final GlobalKey _subtreeKey = GlobalKey();
1765 final PageStorageBucket _storageBucket = PageStorageBucket();
1766
1767 // one of the builders
1768 late OverlayEntry _modalBarrier;
1769 Widget _buildModalBarrier(BuildContext context) {
1770 Widget barrier = buildModalBarrier();
1771 if (filter != null) {
1772 barrier = BackdropFilter(
1773 filter: filter!,
1774 child: barrier,
1775 );
1776 }
1777 barrier = IgnorePointer(
1778 ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates
1779 animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
1780 child: barrier,
1781 );
1782 if (semanticsDismissible && barrierDismissible) {
1783 // To be sorted after the _modalScope.
1784 barrier = Semantics(
1785 sortKey: const OrdinalSortKey(1.0),
1786 child: barrier,
1787 );
1788 }
1789 return barrier;
1790 }
1791
1792 /// Build the barrier for this [ModalRoute], subclasses can override
1793 /// this method to create their own barrier with customized features such as
1794 /// color or accessibility focus size.
1795 ///
1796 /// See also:
1797 /// * [ModalBarrier], which is typically used to build a barrier.
1798 /// * [ModalBottomSheetRoute], which overrides this method to build a
1799 /// customized barrier.
1800 Widget buildModalBarrier() {
1801 Widget barrier;
1802 if (barrierColor != null && barrierColor!.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
1803 assert(barrierColor != barrierColor!.withOpacity(0.0));
1804 final Animation<Color?> color = animation!.drive(
1805 ColorTween(
1806 begin: barrierColor!.withOpacity(0.0),
1807 end: barrierColor, // changedInternalState is called if barrierColor updates
1808 ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
1809 );
1810 barrier = AnimatedModalBarrier(
1811 color: color,
1812 dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
1813 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1814 barrierSemanticsDismissible: semanticsDismissible,
1815 );
1816 } else {
1817 barrier = ModalBarrier(
1818 dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
1819 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1820 barrierSemanticsDismissible: semanticsDismissible,
1821 );
1822 }
1823
1824 return barrier;
1825 }
1826
1827 // We cache the part of the modal scope that doesn't change from frame to
1828 // frame so that we minimize the amount of building that happens.
1829 Widget? _modalScopeCache;
1830
1831 // one of the builders
1832 Widget _buildModalScope(BuildContext context) {
1833 // To be sorted before the _modalBarrier.
1834 return _modalScopeCache ??= Semantics(
1835 sortKey: const OrdinalSortKey(0.0),
1836 child: _ModalScope<T>(
1837 key: _scopeKey,
1838 route: this,
1839 // _ModalScope calls buildTransitions() and buildChild(), defined above
1840 ),
1841 );
1842 }
1843
1844 late OverlayEntry _modalScope;
1845
1846 @override
1847 Iterable<OverlayEntry> createOverlayEntries() {
1848 return <OverlayEntry>[
1849 _modalBarrier = OverlayEntry(builder: _buildModalBarrier),
1850 _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState, canSizeOverlay: opaque),
1851 ];
1852 }
1853
1854 @override
1855 String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
1856}
1857
1858/// A modal route that overlays a widget over the current route.
1859///
1860/// {@macro flutter.widgets.ModalRoute.barrierDismissible}
1861///
1862/// {@tool dartpad}
1863/// This example shows how to create a dialog box that is dismissible.
1864///
1865/// ** See code in examples/api/lib/widgets/routes/popup_route.0.dart **
1866/// {@end-tool}
1867///
1868/// See also:
1869///
1870/// * [ModalRoute], which is the base class for this class.
1871/// * [Navigator.pop], which is used to dismiss the route.
1872abstract class PopupRoute<T> extends ModalRoute<T> {
1873 /// Initializes the [PopupRoute].
1874 PopupRoute({
1875 super.settings,
1876 super.filter,
1877 super.traversalEdgeBehavior,
1878 });
1879
1880 @override
1881 bool get opaque => false;
1882
1883 @override
1884 bool get maintainState => true;
1885
1886 @override
1887 bool get allowSnapshotting => false;
1888}
1889
1890/// A [Navigator] observer that notifies [RouteAware]s of changes to the
1891/// state of their [Route].
1892///
1893/// [RouteObserver] informs subscribers whenever a route of type `R` is pushed
1894/// on top of their own route of type `R` or popped from it. This is for example
1895/// useful to keep track of page transitions, e.g. a `RouteObserver<PageRoute>`
1896/// will inform subscribed [RouteAware]s whenever the user navigates away from
1897/// the current page route to another page route.
1898///
1899/// To be informed about route changes of any type, consider instantiating a
1900/// `RouteObserver<Route>`.
1901///
1902/// ## Type arguments
1903///
1904/// When using more aggressive [lints](https://dart.dev/lints),
1905/// in particular lints such as `always_specify_types`,
1906/// the Dart analyzer will require that certain types
1907/// be given with their type arguments. Since the [Route] class and its
1908/// subclasses have a type argument, this includes the arguments passed to this
1909/// class. Consider using `dynamic` to specify the entire class of routes rather
1910/// than only specific subtypes. For example, to watch for all [ModalRoute]
1911/// variants, the `RouteObserver<ModalRoute<dynamic>>` type may be used.
1912///
1913/// {@tool snippet}
1914///
1915/// To make a [StatefulWidget] aware of its current [Route] state, implement
1916/// [RouteAware] in its [State] and subscribe it to a [RouteObserver]:
1917///
1918/// ```dart
1919/// // Register the RouteObserver as a navigation observer.
1920/// final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
1921///
1922/// void main() {
1923/// runApp(MaterialApp(
1924/// home: Container(),
1925/// navigatorObservers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
1926/// ));
1927/// }
1928///
1929/// class RouteAwareWidget extends StatefulWidget {
1930/// const RouteAwareWidget({super.key});
1931///
1932/// @override
1933/// State<RouteAwareWidget> createState() => RouteAwareWidgetState();
1934/// }
1935///
1936/// // Implement RouteAware in a widget's state and subscribe it to the RouteObserver.
1937/// class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
1938///
1939/// @override
1940/// void didChangeDependencies() {
1941/// super.didChangeDependencies();
1942/// routeObserver.subscribe(this, ModalRoute.of(context)!);
1943/// }
1944///
1945/// @override
1946/// void dispose() {
1947/// routeObserver.unsubscribe(this);
1948/// super.dispose();
1949/// }
1950///
1951/// @override
1952/// void didPush() {
1953/// // Route was pushed onto navigator and is now topmost route.
1954/// }
1955///
1956/// @override
1957/// void didPopNext() {
1958/// // Covering route was popped off the navigator.
1959/// }
1960///
1961/// @override
1962/// Widget build(BuildContext context) => Container();
1963///
1964/// }
1965/// ```
1966/// {@end-tool}
1967class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver {
1968 final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{};
1969
1970 /// Whether this observer is managing changes for the specified route.
1971 ///
1972 /// If asserts are disabled, this method will throw an exception.
1973 @visibleForTesting
1974 bool debugObservingRoute(R route) {
1975 late bool contained;
1976 assert(() {
1977 contained = _listeners.containsKey(route);
1978 return true;
1979 }());
1980 return contained;
1981 }
1982
1983 /// Subscribe [routeAware] to be informed about changes to [route].
1984 ///
1985 /// Going forward, [routeAware] will be informed about qualifying changes
1986 /// to [route], e.g. when [route] is covered by another route or when [route]
1987 /// is popped off the [Navigator] stack.
1988 void subscribe(RouteAware routeAware, R route) {
1989 final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{});
1990 if (subscribers.add(routeAware)) {
1991 routeAware.didPush();
1992 }
1993 }
1994
1995 /// Unsubscribe [routeAware].
1996 ///
1997 /// [routeAware] is no longer informed about changes to its route. If the given argument was
1998 /// subscribed to multiple types, this will unregister it (once) from each type.
1999 void unsubscribe(RouteAware routeAware) {
2000 final List<R> routes = _listeners.keys.toList();
2001 for (final R route in routes) {
2002 final Set<RouteAware>? subscribers = _listeners[route];
2003 if (subscribers != null) {
2004 subscribers.remove(routeAware);
2005 if (subscribers.isEmpty) {
2006 _listeners.remove(route);
2007 }
2008 }
2009 }
2010 }
2011
2012 @override
2013 void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
2014 if (route is R && previousRoute is R) {
2015 final List<RouteAware>? previousSubscribers = _listeners[previousRoute]?.toList();
2016
2017 if (previousSubscribers != null) {
2018 for (final RouteAware routeAware in previousSubscribers) {
2019 routeAware.didPopNext();
2020 }
2021 }
2022
2023 final List<RouteAware>? subscribers = _listeners[route]?.toList();
2024
2025 if (subscribers != null) {
2026 for (final RouteAware routeAware in subscribers) {
2027 routeAware.didPop();
2028 }
2029 }
2030 }
2031 }
2032
2033 @override
2034 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
2035 if (route is R && previousRoute is R) {
2036 final Set<RouteAware>? previousSubscribers = _listeners[previousRoute];
2037
2038 if (previousSubscribers != null) {
2039 for (final RouteAware routeAware in previousSubscribers) {
2040 routeAware.didPushNext();
2041 }
2042 }
2043 }
2044 }
2045}
2046
2047/// An interface for objects that are aware of their current [Route].
2048///
2049/// This is used with [RouteObserver] to make a widget aware of changes to the
2050/// [Navigator]'s session history.
2051abstract mixin class RouteAware {
2052 /// Called when the top route has been popped off, and the current route
2053 /// shows up.
2054 void didPopNext() { }
2055
2056 /// Called when the current route has been pushed.
2057 void didPush() { }
2058
2059 /// Called when the current route has been popped off.
2060 void didPop() { }
2061
2062 /// Called when a new route has been pushed, and the current route is no
2063 /// longer visible.
2064 void didPushNext() { }
2065}
2066
2067/// A general dialog route which allows for customization of the dialog popup.
2068///
2069/// It is used internally by [showGeneralDialog] or can be directly pushed
2070/// onto the [Navigator] stack to enable state restoration. See
2071/// [showGeneralDialog] for a state restoration app example.
2072///
2073/// This function takes a `pageBuilder`, which typically builds a dialog.
2074/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
2075/// returned by the `builder` does not share a context with the location that
2076/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
2077/// custom [StatefulWidget] if the dialog needs to update dynamically.
2078///
2079/// The `barrierDismissible` argument is used to indicate whether tapping on the
2080/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
2081///
2082/// The `barrierColor` argument is used to specify the color of the modal
2083/// barrier that darkens everything below the dialog. If `null`, the default
2084/// color `Colors.black54` is used.
2085///
2086/// The `settings` argument define the settings for this route. See
2087/// [RouteSettings] for details.
2088///
2089/// {@template flutter.widgets.RawDialogRoute}
2090/// A [DisplayFeature] can split the screen into sub-screens. The closest one to
2091/// [anchorPoint] is used to render the content.
2092///
2093/// If no [anchorPoint] is provided, then [Directionality] is used:
2094///
2095/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
2096/// cause the content to appear in the top-left sub-screen.
2097/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
2098/// which will cause the content to appear in the top-right sub-screen.
2099///
2100/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
2101/// widget in the tree, then the widget asserts during build in debug mode.
2102/// {@endtemplate}
2103///
2104/// See also:
2105///
2106/// * [DisplayFeatureSubScreen], which documents the specifics of how
2107/// [DisplayFeature]s can split the screen into sub-screens.
2108/// * [showGeneralDialog], which is a way to display a RawDialogRoute.
2109/// * [showDialog], which is a way to display a DialogRoute.
2110/// * [showCupertinoDialog], which displays an iOS-style dialog.
2111class RawDialogRoute<T> extends PopupRoute<T> {
2112 /// A general dialog route which allows for customization of the dialog popup.
2113 RawDialogRoute({
2114 required RoutePageBuilder pageBuilder,
2115 bool barrierDismissible = true,
2116 Color? barrierColor = const Color(0x80000000),
2117 String? barrierLabel,
2118 Duration transitionDuration = const Duration(milliseconds: 200),
2119 RouteTransitionsBuilder? transitionBuilder,
2120 super.settings,
2121 this.anchorPoint,
2122 super.traversalEdgeBehavior,
2123 }) : _pageBuilder = pageBuilder,
2124 _barrierDismissible = barrierDismissible,
2125 _barrierLabel = barrierLabel,
2126 _barrierColor = barrierColor,
2127 _transitionDuration = transitionDuration,
2128 _transitionBuilder = transitionBuilder;
2129
2130 final RoutePageBuilder _pageBuilder;
2131
2132 @override
2133 bool get barrierDismissible => _barrierDismissible;
2134 final bool _barrierDismissible;
2135
2136 @override
2137 String? get barrierLabel => _barrierLabel;
2138 final String? _barrierLabel;
2139
2140 @override
2141 Color? get barrierColor => _barrierColor;
2142 final Color? _barrierColor;
2143
2144 @override
2145 Duration get transitionDuration => _transitionDuration;
2146 final Duration _transitionDuration;
2147
2148 final RouteTransitionsBuilder? _transitionBuilder;
2149
2150 /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
2151 final Offset? anchorPoint;
2152
2153 @override
2154 Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
2155 return Semantics(
2156 scopesRoute: true,
2157 explicitChildNodes: true,
2158 child: DisplayFeatureSubScreen(
2159 anchorPoint: anchorPoint,
2160 child: _pageBuilder(context, animation, secondaryAnimation),
2161 ),
2162 );
2163 }
2164
2165 @override
2166 Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
2167 if (_transitionBuilder == null) {
2168 // Some default transition.
2169 return FadeTransition(
2170 opacity: CurvedAnimation(
2171 parent: animation,
2172 curve: Curves.linear,
2173 ),
2174 child: child,
2175 );
2176 }
2177 return _transitionBuilder(context, animation, secondaryAnimation, child);
2178 }
2179}
2180
2181/// Displays a dialog above the current contents of the app.
2182///
2183/// This function allows for customization of aspects of the dialog popup.
2184///
2185/// This function takes a `pageBuilder` which is used to build the primary
2186/// content of the route (typically a dialog widget). Content below the dialog
2187/// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder`
2188/// does not share a context with the location that [showGeneralDialog] is
2189/// originally called from. Use a [StatefulBuilder] or a custom
2190/// [StatefulWidget] if the dialog needs to update dynamically.
2191///
2192/// The `context` argument is used to look up the [Navigator] for the
2193/// dialog. It is only used when the method is called. Its corresponding widget
2194/// can be safely removed from the tree before the dialog is closed.
2195///
2196/// The `useRootNavigator` argument is used to determine whether to push the
2197/// dialog to the [Navigator] furthest from or nearest to the given `context`.
2198/// By default, `useRootNavigator` is `true` and the dialog route created by
2199/// this method is pushed to the root navigator.
2200///
2201/// If the application has multiple [Navigator] objects, it may be necessary to
2202/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
2203/// dialog rather than just `Navigator.pop(context, result)`.
2204///
2205/// The `barrierDismissible` argument is used to determine whether this route
2206/// can be dismissed by tapping the modal barrier. This argument defaults
2207/// to false. If `barrierDismissible` is true, a non-null `barrierLabel` must be
2208/// provided.
2209///
2210/// The `barrierLabel` argument is the semantic label used for a dismissible
2211/// barrier. This argument defaults to `null`.
2212///
2213/// The `barrierColor` argument is the color used for the modal barrier. This
2214/// argument defaults to `Color(0x80000000)`.
2215///
2216/// The `transitionDuration` argument is used to determine how long it takes
2217/// for the route to arrive on or leave off the screen. This argument defaults
2218/// to 200 milliseconds.
2219///
2220/// The `transitionBuilder` argument is used to define how the route arrives on
2221/// and leaves off the screen. By default, the transition is a linear fade of
2222/// the page's contents.
2223///
2224/// The `routeSettings` will be used in the construction of the dialog's route.
2225/// See [RouteSettings] for more details.
2226///
2227/// {@macro flutter.widgets.RawDialogRoute}
2228///
2229/// Returns a [Future] that resolves to the value (if any) that was passed to
2230/// [Navigator.pop] when the dialog was closed.
2231///
2232/// ### State Restoration in Dialogs
2233///
2234/// Using this method will not enable state restoration for the dialog. In order
2235/// to enable state restoration for a dialog, use [Navigator.restorablePush]
2236/// or [Navigator.restorablePushNamed] with [RawDialogRoute].
2237///
2238/// For more information about state restoration, see [RestorationManager].
2239///
2240/// {@tool sample}
2241/// This sample demonstrates how to create a restorable dialog. This is
2242/// accomplished by enabling state restoration by specifying
2243/// [WidgetsApp.restorationScopeId] and using [Navigator.restorablePush] to
2244/// push [RawDialogRoute] when the button is tapped.
2245///
2246/// {@macro flutter.widgets.RestorationManager}
2247///
2248/// ** See code in examples/api/lib/widgets/routes/show_general_dialog.0.dart **
2249/// {@end-tool}
2250///
2251/// See also:
2252///
2253/// * [DisplayFeatureSubScreen], which documents the specifics of how
2254/// [DisplayFeature]s can split the screen into sub-screens.
2255/// * [showDialog], which displays a Material-style dialog.
2256/// * [showCupertinoDialog], which displays an iOS-style dialog.
2257Future<T?> showGeneralDialog<T extends Object?>({
2258 required BuildContext context,
2259 required RoutePageBuilder pageBuilder,
2260 bool barrierDismissible = false,
2261 String? barrierLabel,
2262 Color barrierColor = const Color(0x80000000),
2263 Duration transitionDuration = const Duration(milliseconds: 200),
2264 RouteTransitionsBuilder? transitionBuilder,
2265 bool useRootNavigator = true,
2266 RouteSettings? routeSettings,
2267 Offset? anchorPoint,
2268}) {
2269 assert(!barrierDismissible || barrierLabel != null);
2270 return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
2271 pageBuilder: pageBuilder,
2272 barrierDismissible: barrierDismissible,
2273 barrierLabel: barrierLabel,
2274 barrierColor: barrierColor,
2275 transitionDuration: transitionDuration,
2276 transitionBuilder: transitionBuilder,
2277 settings: routeSettings,
2278 anchorPoint: anchorPoint,
2279 ));
2280}
2281
2282/// Signature for the function that builds a route's primary contents.
2283/// Used in [PageRouteBuilder] and [showGeneralDialog].
2284///
2285/// See [ModalRoute.buildPage] for complete definition of the parameters.
2286typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
2287
2288/// Signature for the function that builds a route's transitions.
2289/// Used in [PageRouteBuilder] and [showGeneralDialog].
2290///
2291/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
2292typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
2293
2294/// A callback type for informing that a navigation pop has been invoked,
2295/// whether or not it was handled successfully.
2296///
2297/// Accepts a didPop boolean indicating whether or not back navigation
2298/// succeeded.
2299typedef PopInvokedCallback = void Function(bool didPop);
2300
2301/// Allows listening to and preventing pops.
2302///
2303/// Can be registered in [ModalRoute] to listen to pops with [onPopInvoked] or
2304/// to enable/disable them with [canPopNotifier].
2305///
2306/// See also:
2307///
2308/// * [PopScope], which provides similar functionality in a widget.
2309/// * [ModalRoute.registerPopEntry], which unregisters instances of this.
2310/// * [ModalRoute.unregisterPopEntry], which unregisters instances of this.
2311abstract class PopEntry {
2312 /// {@macro flutter.widgets.PopScope.onPopInvoked}
2313 PopInvokedCallback? get onPopInvoked;
2314
2315 /// {@macro flutter.widgets.PopScope.canPop}
2316 ValueListenable<bool> get canPopNotifier;
2317
2318 @override
2319 String toString() {
2320 return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvoked';
2321 }
2322}
2323