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