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