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';
13library;
14
15import 'package:flutter/foundation.dart';
16
17import '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.
35class 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
41class _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.
70class 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.
170class 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