1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:flutter/material.dart'; |
6 | /// |
7 | /// @docImport 'routes.dart'; |
8 | /// @docImport 'scroll_controller.dart'; |
9 | /// @docImport 'scroll_position.dart'; |
10 | /// @docImport 'scroll_view.dart'; |
11 | /// @docImport 'scrollable.dart'; |
12 | /// @docImport 'single_child_scroll_view.dart'; |
13 | library; |
14 | |
15 | import 'package:flutter/foundation.dart'; |
16 | |
17 | import 'framework.dart'; |
18 | |
19 | // Examples can assume: |
20 | // late BuildContext context; |
21 | |
22 | /// A [Key] that can be used to persist the widget state in storage after the |
23 | /// destruction and will be restored when recreated. |
24 | /// |
25 | /// Each key with its value plus the ancestor chain of other [PageStorageKey]s |
26 | /// need to be unique within the widget's closest ancestor [PageStorage]. To |
27 | /// make it possible for a saved value to be found when a widget is recreated, |
28 | /// the key's value must not be objects whose identity will change each time the |
29 | /// widget is created. |
30 | /// |
31 | /// See also: |
32 | /// |
33 | /// * [PageStorage], which manages the data storage for widgets using |
34 | /// [PageStorageKey]s. |
35 | class PageStorageKey<T> extends ValueKey<T> { |
36 | /// Creates a [ValueKey] that defines where [PageStorage] values will be saved. |
37 | const PageStorageKey(super.value); |
38 | } |
39 | |
40 | @immutable |
41 | class _StorageEntryIdentifier { |
42 | const _StorageEntryIdentifier(this.keys); |
43 | |
44 | final List<PageStorageKey<dynamic>> keys; |
45 | |
46 | bool get isNotEmpty => keys.isNotEmpty; |
47 | |
48 | @override |
49 | bool operator ==(Object other) { |
50 | if (other.runtimeType != runtimeType) { |
51 | return false; |
52 | } |
53 | return other is _StorageEntryIdentifier |
54 | && listEquals<PageStorageKey<dynamic>>(other.keys, keys); |
55 | } |
56 | |
57 | @override |
58 | int get hashCode => Object.hashAll(keys); |
59 | |
60 | @override |
61 | String toString() { |
62 | return 'StorageEntryIdentifier( ${keys.join(":" )})' ; |
63 | } |
64 | } |
65 | |
66 | /// A storage bucket associated with a page in an app. |
67 | /// |
68 | /// Useful for storing per-page state that persists across navigations from one |
69 | /// page to another. |
70 | class PageStorageBucket { |
71 | static bool _maybeAddKey(BuildContext context, List<PageStorageKey<dynamic>> keys) { |
72 | final Widget widget = context.widget; |
73 | final Key? key = widget.key; |
74 | if (key is PageStorageKey) { |
75 | keys.add(key); |
76 | } |
77 | return widget is! PageStorage; |
78 | } |
79 | |
80 | List<PageStorageKey<dynamic>> _allKeys(BuildContext context) { |
81 | final List<PageStorageKey<dynamic>> keys = <PageStorageKey<dynamic>>[]; |
82 | if (_maybeAddKey(context, keys)) { |
83 | context.visitAncestorElements((Element element) { |
84 | return _maybeAddKey(element, keys); |
85 | }); |
86 | } |
87 | return keys; |
88 | } |
89 | |
90 | _StorageEntryIdentifier _computeIdentifier(BuildContext context) { |
91 | return _StorageEntryIdentifier(_allKeys(context)); |
92 | } |
93 | |
94 | Map<Object, dynamic>? _storage; |
95 | |
96 | /// Write the given data into this page storage bucket using the |
97 | /// specified identifier or an identifier computed from the given context. |
98 | /// The computed identifier is based on the [PageStorageKey]s |
99 | /// found in the path from context to the [PageStorage] widget that |
100 | /// owns this page storage bucket. |
101 | /// |
102 | /// If an explicit identifier is not provided and no [PageStorageKey]s |
103 | /// are found, then the `data` is not saved. |
104 | void writeState(BuildContext context, dynamic data, { Object? identifier }) { |
105 | _storage ??= <Object, dynamic>{}; |
106 | if (identifier != null) { |
107 | _storage![identifier] = data; |
108 | } else { |
109 | final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context); |
110 | if (contextIdentifier.isNotEmpty) { |
111 | _storage![contextIdentifier] = data; |
112 | } |
113 | } |
114 | } |
115 | |
116 | /// Read given data from into this page storage bucket using the specified |
117 | /// identifier or an identifier computed from the given context. |
118 | /// The computed identifier is based on the [PageStorageKey]s |
119 | /// found in the path from context to the [PageStorage] widget that |
120 | /// owns this page storage bucket. |
121 | /// |
122 | /// If an explicit identifier is not provided and no [PageStorageKey]s |
123 | /// are found, then null is returned. |
124 | dynamic readState(BuildContext context, { Object? identifier }) { |
125 | if (_storage == null) { |
126 | return null; |
127 | } |
128 | if (identifier != null) { |
129 | return _storage![identifier]; |
130 | } |
131 | final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context); |
132 | return contextIdentifier.isNotEmpty ? _storage![contextIdentifier] : null; |
133 | } |
134 | } |
135 | |
136 | /// Establish a subtree in which widgets can opt into persisting states after |
137 | /// being destroyed. |
138 | /// |
139 | /// [PageStorage] is used to save and restore values that can outlive the widget. |
140 | /// For example, when multiple pages are grouped in tabs, when a page is |
141 | /// switched out, its widget is destroyed and its state is lost. By adding a |
142 | /// [PageStorage] at the root and adding a [PageStorageKey] to each page, some of the |
143 | /// page's state (e.g. the scroll position of a [Scrollable] widget) will be stored |
144 | /// automatically in its closest ancestor [PageStorage], and restored when it's |
145 | /// switched back. |
146 | /// |
147 | /// Usually you don't need to explicitly use a [PageStorage], since it's already |
148 | /// included in routes. |
149 | /// |
150 | /// [PageStorageKey] is used by [Scrollable] if [ScrollController.keepScrollOffset] |
151 | /// is enabled to save their [ScrollPosition]s. When more than one scrollable |
152 | /// ([ListView], [SingleChildScrollView], [TextField], etc.) appears within the |
153 | /// widget's closest ancestor [PageStorage] (such as within the same route), to |
154 | /// save all of their positions independently, one must give each of them unique |
155 | /// [PageStorageKey]s, or set the `keepScrollOffset` property of some such |
156 | /// widgets to false to prevent saving. |
157 | /// |
158 | /// {@tool dartpad} |
159 | /// This sample shows how to explicitly use a [PageStorage] to |
160 | /// store the states of its children pages. Each page includes a scrollable |
161 | /// list, whose position is preserved when switching between the tabs thanks to |
162 | /// the help of [PageStorageKey]. |
163 | /// |
164 | /// ** See code in examples/api/lib/widgets/page_storage/page_storage.0.dart ** |
165 | /// {@end-tool} |
166 | /// |
167 | /// See also: |
168 | /// |
169 | /// * [ModalRoute], which includes this class. |
170 | class PageStorage extends StatelessWidget { |
171 | /// Creates a widget that provides a storage bucket for its descendants. |
172 | const PageStorage({ |
173 | super.key, |
174 | required this.bucket, |
175 | required this.child, |
176 | }); |
177 | |
178 | /// The widget below this widget in the tree. |
179 | /// |
180 | /// {@macro flutter.widgets.ProxyWidget.child} |
181 | final Widget child; |
182 | |
183 | /// The page storage bucket to use for this subtree. |
184 | final PageStorageBucket bucket; |
185 | |
186 | /// The [PageStorageBucket] from the closest instance of a [PageStorage] |
187 | /// widget that encloses the given context. |
188 | /// |
189 | /// Returns null if none exists. |
190 | /// |
191 | /// Typical usage is as follows: |
192 | /// |
193 | /// ```dart |
194 | /// PageStorageBucket? bucket = PageStorage.of(context); |
195 | /// ``` |
196 | /// |
197 | /// This method can be expensive (it walks the element tree). |
198 | /// |
199 | /// See also: |
200 | /// |
201 | /// * [PageStorage.of], which is similar to this method, but |
202 | /// asserts if no [PageStorage] ancestor is found. |
203 | static PageStorageBucket? maybeOf(BuildContext context) { |
204 | final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>(); |
205 | return widget?.bucket; |
206 | } |
207 | |
208 | /// The [PageStorageBucket] from the closest instance of a [PageStorage] |
209 | /// widget that encloses the given context. |
210 | /// |
211 | /// If no ancestor is found, this method will assert in debug mode, and throw |
212 | /// an exception in release mode. |
213 | /// |
214 | /// Typical usage is as follows: |
215 | /// |
216 | /// ```dart |
217 | /// PageStorageBucket bucket = PageStorage.of(context); |
218 | /// ``` |
219 | /// |
220 | /// This method can be expensive (it walks the element tree). |
221 | /// |
222 | /// See also: |
223 | /// |
224 | /// * [PageStorage.maybeOf], which is similar to this method, but |
225 | /// returns null if no [PageStorage] ancestor is found. |
226 | static PageStorageBucket of(BuildContext context) { |
227 | final PageStorageBucket? bucket = maybeOf(context); |
228 | assert(() { |
229 | if (bucket == null) { |
230 | throw FlutterError( |
231 | 'PageStorage.of() was called with a context that does not contain a ' |
232 | 'PageStorage widget.\n' |
233 | 'No PageStorage widget ancestor could be found starting from the ' |
234 | 'context that was passed to PageStorage.of(). This can happen ' |
235 | 'because you are using a widget that looks for a PageStorage ' |
236 | 'ancestor, but no such ancestor exists.\n' |
237 | 'The context used was:\n' |
238 | ' $context' , |
239 | ); |
240 | } |
241 | return true; |
242 | }()); |
243 | return bucket!; |
244 | } |
245 | |
246 | @override |
247 | Widget build(BuildContext context) => child; |
248 | } |
249 | |