1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6
7import 'framework.dart';
8import 'navigator.dart';
9import 'routes.dart';
10
11/// Manages system back gestures.
12///
13/// The [canPop] parameter can be used to disable system back gestures. Defaults
14/// to true, meaning that back gestures happen as usual.
15///
16/// The [onPopInvoked] parameter reports when system back gestures occur,
17/// regardless of whether or not they were successful.
18///
19/// If [canPop] is false, then a system back gesture will not pop the route off
20/// of the enclosing [Navigator]. [onPopInvoked] will still be called, and
21/// `didPop` will be `false`.
22///
23/// If [canPop] is true, then a system back gesture will cause the enclosing
24/// [Navigator] to receive a pop as usual. [onPopInvoked] will be called with
25/// `didPop` as `true`, unless the pop failed for reasons unrelated to
26/// [PopScope], in which case it will be `false`.
27///
28/// {@tool dartpad}
29/// This sample demonstrates how to use this widget to handle nested navigation
30/// in a bottom navigation bar.
31///
32/// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.0.dart **
33/// {@end-tool}
34///
35/// See also:
36///
37/// * [NavigatorPopHandler], which is a less verbose way to handle system back
38/// gestures in simple cases of nested [Navigator]s.
39/// * [Form.canPop] and [Form.onPopInvoked], which can be used to handle system
40/// back gestures in the case of a form with unsaved data.
41/// * [ModalRoute.registerPopEntry] and [ModalRoute.unregisterPopEntry],
42/// which this widget uses to integrate with Flutter's navigation system.
43class PopScope extends StatefulWidget {
44 /// Creates a widget that registers a callback to veto attempts by the user to
45 /// dismiss the enclosing [ModalRoute].
46 const PopScope({
47 super.key,
48 required this.child,
49 this.canPop = true,
50 this.onPopInvoked,
51 });
52
53 /// The widget below this widget in the tree.
54 ///
55 /// {@macro flutter.widgets.ProxyWidget.child}
56 final Widget child;
57
58 /// {@template flutter.widgets.PopScope.onPopInvoked}
59 /// Called after a route pop was handled.
60 /// {@endtemplate}
61 ///
62 /// It's not possible to prevent the pop from happening at the time that this
63 /// method is called; the pop has already happened. Use [canPop] to
64 /// disable pops in advance.
65 ///
66 /// This will still be called even when the pop is canceled. A pop is canceled
67 /// when the relevant [Route.popDisposition] returns false, such as when
68 /// [canPop] is set to false on a [PopScope]. The `didPop` parameter
69 /// indicates whether or not the back navigation actually happened
70 /// successfully.
71 ///
72 /// See also:
73 ///
74 /// * [Route.onPopInvoked], which is similar.
75 final PopInvokedCallback? onPopInvoked;
76
77 /// {@template flutter.widgets.PopScope.canPop}
78 /// When false, blocks the current route from being popped.
79 ///
80 /// This includes the root route, where upon popping, the Flutter app would
81 /// exit.
82 ///
83 /// If multiple [PopScope] widgets appear in a route's widget subtree, then
84 /// each and every `canPop` must be `true` in order for the route to be
85 /// able to pop.
86 ///
87 /// [Android's predictive back](https://developer.android.com/guide/navigation/predictive-back-gesture)
88 /// feature will not animate when this boolean is false.
89 /// {@endtemplate}
90 final bool canPop;
91
92 @override
93 State<PopScope> createState() => _PopScopeState();
94}
95
96class _PopScopeState extends State<PopScope> implements PopEntry {
97 ModalRoute<dynamic>? _route;
98
99 @override
100 PopInvokedCallback? get onPopInvoked => widget.onPopInvoked;
101
102 @override
103 late final ValueNotifier<bool> canPopNotifier;
104
105 @override
106 void initState() {
107 super.initState();
108 canPopNotifier = ValueNotifier<bool>(widget.canPop);
109 }
110
111 @override
112 void didChangeDependencies() {
113 super.didChangeDependencies();
114 final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
115 if (nextRoute != _route) {
116 _route?.unregisterPopEntry(this);
117 _route = nextRoute;
118 _route?.registerPopEntry(this);
119 }
120 }
121
122 @override
123 void didUpdateWidget(PopScope oldWidget) {
124 super.didUpdateWidget(oldWidget);
125 canPopNotifier.value = widget.canPop;
126 }
127
128 @override
129 void dispose() {
130 _route?.unregisterPopEntry(this);
131 canPopNotifier.dispose();
132 super.dispose();
133 }
134
135 @override
136 Widget build(BuildContext context) => widget.child;
137}
138