page_storage.dart 8.19 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'package:flutter/foundation.dart';

7 8
import 'framework.dart';

9 10 11
// Examples can assume:
// late BuildContext context;

12 13
/// A [Key] that can be used to persist the widget state in storage after the
/// destruction and will be restored when recreated.
14
///
15 16 17 18 19
/// Each key with its value plus the ancestor chain of other [PageStorageKey]s
/// need to be unique within the widget's closest ancestor [PageStorage]. To
/// make it possible for a saved value to be found when a widget is recreated,
/// the key's value must not be objects whose identity will change each time the
/// widget is created.
20
///
21
/// See also:
22
///
23 24
///  * [PageStorage], which manages the data storage for widgets using
///    [PageStorageKey]s.
25 26
class PageStorageKey<T> extends ValueKey<T> {
  /// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
27
  const PageStorageKey(super.value);
28
}
29

30
@immutable
31
class _StorageEntryIdentifier {
32
  const _StorageEntryIdentifier(this.keys);
33

34
  final List<PageStorageKey<dynamic>> keys;
35

36 37
  bool get isNotEmpty => keys.isNotEmpty;

38
  @override
39
  bool operator ==(Object other) {
40
    if (other.runtimeType != runtimeType) {
41
      return false;
42
    }
43 44
    return other is _StorageEntryIdentifier
        && 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 57 58 59
/// A storage bucket associated with a page in an app.
///
/// Useful for storing per-page state that persists across navigations from one
/// page to another.
60
class PageStorageBucket {
61 62
  static bool _maybeAddKey(BuildContext context, List<PageStorageKey<dynamic>> keys) {
    final Widget widget = context.widget;
63
    final Key? key = widget.key;
64
    if (key is PageStorageKey) {
65
      keys.add(key);
66
    }
67 68 69 70 71 72
    return widget is! PageStorage;
  }

  List<PageStorageKey<dynamic>> _allKeys(BuildContext context) {
    final List<PageStorageKey<dynamic>> keys = <PageStorageKey<dynamic>>[];
    if (_maybeAddKey(context, keys)) {
73
      context.visitAncestorElements((Element element) {
74
        return _maybeAddKey(element, keys);
75 76
      });
    }
77 78 79 80
    return keys;
  }

  _StorageEntryIdentifier _computeIdentifier(BuildContext context) {
81
    return _StorageEntryIdentifier(_allKeys(context));
82 83
  }

84
  Map<Object, dynamic>? _storage;
85

86 87 88 89 90
  /// Write the given data into this page storage bucket using the
  /// specified identifier or an identifier computed from the given context.
  /// The computed identifier is based on the [PageStorageKey]s
  /// found in the path from context to the [PageStorage] widget that
  /// owns this page storage bucket.
91
  ///
92 93
  /// If an explicit identifier is not provided and no [PageStorageKey]s
  /// 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 99
    } else {
      final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
100
      if (contextIdentifier.isNotEmpty) {
101
        _storage![contextIdentifier] = data;
102
      }
103
    }
104
  }
105

106 107 108 109 110 111 112 113
  /// Read given data from into this page storage bucket using the specified
  /// identifier or an identifier computed from the given context.
  /// The computed identifier is based on the [PageStorageKey]s
  /// found in the path from context to the [PageStorage] widget that
  /// owns this page storage bucket.
  ///
  /// If an explicit identifier is not provided and no [PageStorageKey]s
  /// 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 127 128 129 130 131 132 133 134 135 136 137 138 139
/// Establish a subtree in which widgets can opt into persisting states after
/// being destroyed.
///
/// [PageStorage] is used to save and restore values that can outlive the widget.
/// For example, when multiple pages are grouped in tabs, when a page is
/// switched out, its widget is destroyed and its state is lost. By adding a
/// [PageStorage] at the root and adding a [PageStorageKey] to each page, some of the
/// page's state (e.g. the scroll position of a [Scrollable] widget) will be stored
/// automatically in its closest ancestor [PageStorage], and restored when it's
/// switched back.
///
/// Usually you don't need to explicitly use a [PageStorage], since it's already
/// included in routes.
///
140
/// [PageStorageKey] is used by [Scrollable] if [ScrollController.keepScrollOffset]
141 142 143 144 145 146
/// is enabled to save their [ScrollPosition]s. When more than one scrollable
/// ([ListView], [SingleChildScrollView], [TextField], etc.) appears within the
/// widget's closest ancestor [PageStorage] (such as within the same route), to
/// save all of their positions independently, one must give each of them unique
/// [PageStorageKey]s, or set the `keepScrollOffset` property of some such
/// widgets to false to prevent saving.
147
///
148
/// {@tool dartpad}
149 150 151 152 153
/// This sample shows how to explicitly use a [PageStorage] to
/// store the states of its children pages. Each page includes a scrollable
/// list, whose position is preserved when switching between the tabs thanks to
/// the help of [PageStorageKey].
///
154
/// ** See code in examples/api/lib/widgets/page_storage/page_storage.0.dart **
155 156 157 158 159
/// {@end-tool}
///
/// See also:
///
///  * [ModalRoute], which includes this class.
160
class PageStorage extends StatelessWidget {
161 162 163
  /// Creates a widget that provides a storage bucket for its descendants.
  ///
  /// The [bucket] argument must not be null.
164
  const PageStorage({
165
    super.key,
166 167
    required this.bucket,
    required this.child,
168
  });
169

170
  /// The widget below this widget in the tree.
171
  ///
172
  /// {@macro flutter.widgets.ProxyWidget.child}
173
  final Widget child;
174 175

  /// The page storage bucket to use for this subtree.
176 177
  final PageStorageBucket bucket;

178 179
  /// The [PageStorageBucket] from the closest instance of a [PageStorage]
  /// widget that encloses the given context.
180
  ///
181
  /// Returns null if none exists.
182 183 184 185
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
186
  /// PageStorageBucket? bucket = PageStorage.of(context);
187
  /// ```
188 189
  ///
  /// This method can be expensive (it walks the element tree).
190 191 192 193 194 195
  ///
  /// See also:
  ///
  /// * [PageStorage.of], which is similar to this method, but
  ///   asserts if no [PageStorage] ancestor is found.
  static PageStorageBucket? maybeOf(BuildContext context) {
196
    final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>();
Hixie's avatar
Hixie committed
197
    return widget?.bucket;
198 199
  }

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
  /// The [PageStorageBucket] from the closest instance of a [PageStorage]
  /// widget that encloses the given context.
  ///
  /// If no ancestor is found, this method will assert in debug mode, and throw
  /// an exception in release mode.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// PageStorageBucket bucket = PageStorage.of(context);
  /// ```
  ///
  /// This method can be expensive (it walks the element tree).
  ///
  /// See also:
  ///
  /// * [PageStorage.maybeOf], which is similar to this method, but
  ///   returns null if no [PageStorage] ancestor is found.
  static PageStorageBucket of(BuildContext context) {
    final PageStorageBucket? bucket = maybeOf(context);
    assert(() {
      if (bucket == null) {
        throw FlutterError(
          'PageStorage.of() was called with a context that does not contain a '
          'PageStorage widget.\n'
          'No PageStorage widget ancestor could be found starting from the '
          'context that was passed to PageStorage.of(). This can happen '
          'because you are using a widget that looks for a PageStorage '
          'ancestor, but no such ancestor exists.\n'
          'The context used was:\n'
          '  $context',
        );
      }
      return true;
    }());
    return bucket!;
  }

238
  @override
239 240
  Widget build(BuildContext context) => child;
}