blob: 6f203a232f6904d523f2def51619f743cd88fbd4 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'framework.dart';
/// A key can be used to persist the widget state in storage after
/// the destruction and will be restored when recreated.
///
/// 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.
///
/// See also:
///
/// * [PageStorage], which is the closet ancestor for [PageStorageKey].
class PageStorageKey<T> extends ValueKey<T> {
/// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
const PageStorageKey(T value) : super(value);
}
@immutable
class _StorageEntryIdentifier {
const _StorageEntryIdentifier(this.keys)
: assert(keys != null);
final List<PageStorageKey<dynamic>> keys;
bool get isNotEmpty => keys.isNotEmpty;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is _StorageEntryIdentifier
&& listEquals<PageStorageKey<dynamic>>(other.keys, keys);
}
@override
int get hashCode => hashList(keys);
@override
String toString() {
return 'StorageEntryIdentifier(${keys.join(":")})';
}
}
/// 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.
class PageStorageBucket {
static bool _maybeAddKey(BuildContext context, List<PageStorageKey<dynamic>> keys) {
final Widget widget = context.widget;
final Key? key = widget.key;
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)) {
context.visitAncestorElements((Element element) {
return _maybeAddKey(element, keys);
});
}
return keys;
}
_StorageEntryIdentifier _computeIdentifier(BuildContext context) {
return _StorageEntryIdentifier(_allKeys(context));
}
Map<Object, dynamic>? _storage;
/// 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.
///
/// If an explicit identifier is not provided and no [PageStorageKey]s
/// are found, then the `data` is not saved.
void writeState(BuildContext context, dynamic data, { Object? identifier }) {
_storage ??= <Object, dynamic>{};
if (identifier != null) {
_storage![identifier] = data;
} else {
final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
if (contextIdentifier.isNotEmpty)
_storage![contextIdentifier] = data;
}
}
/// 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.
dynamic readState(BuildContext context, { Object? identifier }) {
if (_storage == null)
return null;
if (identifier != null)
return _storage![identifier];
final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context);
return contextIdentifier.isNotEmpty ? _storage![contextIdentifier] : null;
}
}
/// 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.
///
/// [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.
///
/// {@tool dartpad --template=freeform}
///
/// 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].
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart main
/// void main() => runApp(const MyApp());
/// ```
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// const MyApp({Key? key}) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return const MaterialApp(
/// home: MyHomePage(),
/// );
/// }
/// }
///
/// class MyHomePage extends StatefulWidget {
/// const MyHomePage({Key? key}) : super(key: key);
///
/// @override
/// _MyHomePageState createState() => _MyHomePageState();
/// }
///
/// class _MyHomePageState extends State<MyHomePage> {
/// final List<Widget> pages = const <Widget>[
/// ColorBoxPage(
/// key: PageStorageKey<String>('pageOne'),
/// ),
/// ColorBoxPage(
/// key: PageStorageKey<String>('pageTwo'),
/// )
/// ];
/// int currentTab = 0;
/// final PageStorageBucket _bucket = PageStorageBucket();
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Persistence Example'),
/// ),
/// body: PageStorage(
/// child: pages[currentTab],
/// bucket: _bucket,
/// ),
/// bottomNavigationBar: BottomNavigationBar(
/// currentIndex: currentTab,
/// onTap: (int index) {
/// setState(() {
/// currentTab = index;
/// });
/// },
/// items: const <BottomNavigationBarItem>[
/// BottomNavigationBarItem(
/// icon: Icon(Icons.home),
/// label: 'page 1',
/// ),
/// BottomNavigationBarItem(
/// icon: Icon(Icons.settings),
/// label: 'page2',
/// ),
/// ],
/// ),
/// );
/// }
/// }
///
/// class ColorBoxPage extends StatelessWidget {
/// const ColorBoxPage({Key? key}) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return ListView.builder(
/// itemExtent: 250.0,
/// itemBuilder: (BuildContext context, int index) => Container(
/// padding: const EdgeInsets.all(10.0),
/// child: Material(
/// color: index.isEven ? Colors.cyan : Colors.deepOrange,
/// child: Center(
/// child: Text(index.toString()),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [ModalRoute], which includes this class.
class PageStorage extends StatelessWidget {
/// Creates a widget that provides a storage bucket for its descendants.
///
/// The [bucket] argument must not be null.
const PageStorage({
Key? key,
required this.bucket,
required this.child,
}) : assert(bucket != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// The page storage bucket to use for this subtree.
final PageStorageBucket bucket;
/// The bucket from the closest instance of this class that encloses the given context.
///
/// Returns null if none exists.
///
/// Typical usage is as follows:
///
/// ```dart
/// PageStorageBucket bucket = PageStorage.of(context);
/// ```
///
/// This method can be expensive (it walks the element tree).
static PageStorageBucket? of(BuildContext context) {
final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>();
return widget?.bucket;
}
@override
Widget build(BuildContext context) => child;
}