page_storage.dart 6.64 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
/// A key can be used to persist the widget state in storage after
/// the destruction and will be restored when recreated.
11
///
12 13 14 15
/// Each key with its value plus the ancestor chain of other PageStorageKeys 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.
16
///
17
/// See also:
18
///
19
///  * [PageStorage], which is the closet ancestor for [PageStorageKey].
20 21
class PageStorageKey<T> extends ValueKey<T> {
  /// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
22
  const PageStorageKey(super.value);
23
}
24

25
@immutable
26
class _StorageEntryIdentifier {
27
  const _StorageEntryIdentifier(this.keys)
28
    : assert(keys != null);
29

30
  final List<PageStorageKey<dynamic>> keys;
31

32 33
  bool get isNotEmpty => keys.isNotEmpty;

34
  @override
35
  bool operator ==(Object other) {
36
    if (other.runtimeType != runtimeType)
37
      return false;
38 39
    return other is _StorageEntryIdentifier
        && listEquals<PageStorageKey<dynamic>>(other.keys, keys);
40
  }
41 42

  @override
43
  int get hashCode => Object.hashAll(keys);
44

45
  @override
46
  String toString() {
47
    return 'StorageEntryIdentifier(${keys.join(":")})';
48
  }
49 50
}

51 52 53 54
/// 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.
55
class PageStorageBucket {
56 57
  static bool _maybeAddKey(BuildContext context, List<PageStorageKey<dynamic>> keys) {
    final Widget widget = context.widget;
58
    final Key? key = widget.key;
59 60 61 62 63 64 65 66
    if (key is PageStorageKey)
      keys.add(key);
    return widget is! PageStorage;
  }

  List<PageStorageKey<dynamic>> _allKeys(BuildContext context) {
    final List<PageStorageKey<dynamic>> keys = <PageStorageKey<dynamic>>[];
    if (_maybeAddKey(context, keys)) {
67
      context.visitAncestorElements((Element element) {
68
        return _maybeAddKey(element, keys);
69 70
      });
    }
71 72 73 74
    return keys;
  }

  _StorageEntryIdentifier _computeIdentifier(BuildContext context) {
75
    return _StorageEntryIdentifier(_allKeys(context));
76 77
  }

78
  Map<Object, dynamic>? _storage;
79

80 81 82 83 84
  /// 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.
85
  ///
86 87
  /// If an explicit identifier is not provided and no [PageStorageKey]s
  /// are found, then the `data` is not saved.
88
  void writeState(BuildContext context, dynamic data, { Object? identifier }) {
89
    _storage ??= <Object, dynamic>{};
90
    if (identifier != null) {
91
      _storage![identifier] = data;
92 93 94
    } else {
      final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
      if (contextIdentifier.isNotEmpty)
95
        _storage![contextIdentifier] = data;
96
    }
97
  }
98

99 100 101 102 103 104 105 106
  /// 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.
107
  dynamic readState(BuildContext context, { Object? identifier }) {
108 109 110
    if (_storage == null)
      return null;
    if (identifier != null)
111
      return _storage![identifier];
112
    final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
113
    return contextIdentifier.isNotEmpty ? _storage![contextIdentifier] : null;
114 115 116
  }
}

117 118 119 120 121 122 123 124 125 126 127 128 129 130
/// 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.
///
131 132 133 134 135 136 137
/// [PageStorageKey] is used by [Scrollable] if [ScrollController.keepScrollOffset]
/// 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),
/// if you want to save all of their positions independently,
/// you should give each of them unique [PageStorageKey]s, or set some of their
/// `keepScrollOffset` false to prevent saving.
138
///
139
/// {@tool dartpad}
140 141 142 143 144
/// 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].
///
145
/// ** See code in examples/api/lib/widgets/page_storage/page_storage.0.dart **
146 147 148 149 150
/// {@end-tool}
///
/// See also:
///
///  * [ModalRoute], which includes this class.
151
class PageStorage extends StatelessWidget {
152 153 154
  /// Creates a widget that provides a storage bucket for its descendants.
  ///
  /// The [bucket] argument must not be null.
155
  const PageStorage({
156
    super.key,
157 158
    required this.bucket,
    required this.child,
159
  }) : assert(bucket != null);
160

161
  /// The widget below this widget in the tree.
162
  ///
163
  /// {@macro flutter.widgets.ProxyWidget.child}
164
  final Widget child;
165 166

  /// The page storage bucket to use for this subtree.
167 168
  final PageStorageBucket bucket;

169 170
  /// The bucket from the closest instance of this class that encloses the given context.
  ///
171
  /// Returns null if none exists.
172 173 174 175 176 177
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// PageStorageBucket bucket = PageStorage.of(context);
  /// ```
178 179
  ///
  /// This method can be expensive (it walks the element tree).
180 181
  static PageStorageBucket? of(BuildContext context) {
    final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>();
Hixie's avatar
Hixie committed
182
    return widget?.bucket;
183 184
  }

185
  @override
186 187
  Widget build(BuildContext context) => child;
}