| 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({super.key, required this.bucket, required this.child}); |
| 173 | |
| 174 | /// The widget below this widget in the tree. |
| 175 | /// |
| 176 | /// {@macro flutter.widgets.ProxyWidget.child} |
| 177 | final Widget child; |
| 178 | |
| 179 | /// The page storage bucket to use for this subtree. |
| 180 | final PageStorageBucket bucket; |
| 181 | |
| 182 | /// The [PageStorageBucket] from the closest instance of a [PageStorage] |
| 183 | /// widget that encloses the given context. |
| 184 | /// |
| 185 | /// Returns null if none exists. |
| 186 | /// |
| 187 | /// Typical usage is as follows: |
| 188 | /// |
| 189 | /// ```dart |
| 190 | /// PageStorageBucket? bucket = PageStorage.of(context); |
| 191 | /// ``` |
| 192 | /// |
| 193 | /// This method can be expensive (it walks the element tree). |
| 194 | /// |
| 195 | /// See also: |
| 196 | /// |
| 197 | /// * [PageStorage.of], which is similar to this method, but |
| 198 | /// asserts if no [PageStorage] ancestor is found. |
| 199 | static PageStorageBucket? maybeOf(BuildContext context) { |
| 200 | final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>(); |
| 201 | return widget?.bucket; |
| 202 | } |
| 203 | |
| 204 | /// The [PageStorageBucket] from the closest instance of a [PageStorage] |
| 205 | /// widget that encloses the given context. |
| 206 | /// |
| 207 | /// If no ancestor is found, this method will assert in debug mode, and throw |
| 208 | /// an exception in release mode. |
| 209 | /// |
| 210 | /// Typical usage is as follows: |
| 211 | /// |
| 212 | /// ```dart |
| 213 | /// PageStorageBucket bucket = PageStorage.of(context); |
| 214 | /// ``` |
| 215 | /// |
| 216 | /// This method can be expensive (it walks the element tree). |
| 217 | /// |
| 218 | /// See also: |
| 219 | /// |
| 220 | /// * [PageStorage.maybeOf], which is similar to this method, but |
| 221 | /// returns null if no [PageStorage] ancestor is found. |
| 222 | static PageStorageBucket of(BuildContext context) { |
| 223 | final PageStorageBucket? bucket = maybeOf(context); |
| 224 | assert(() { |
| 225 | if (bucket == null) { |
| 226 | throw FlutterError( |
| 227 | 'PageStorage.of() was called with a context that does not contain a ' |
| 228 | 'PageStorage widget.\n' |
| 229 | 'No PageStorage widget ancestor could be found starting from the ' |
| 230 | 'context that was passed to PageStorage.of(). This can happen ' |
| 231 | 'because you are using a widget that looks for a PageStorage ' |
| 232 | 'ancestor, but no such ancestor exists.\n' |
| 233 | 'The context used was:\n' |
| 234 | ' $context' , |
| 235 | ); |
| 236 | } |
| 237 | return true; |
| 238 | }()); |
| 239 | return bucket!; |
| 240 | } |
| 241 | |
| 242 | @override |
| 243 | Widget build(BuildContext context) => child; |
| 244 | } |
| 245 | |