| // 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); |
| /// ``` |
| static PageStorageBucket? of(BuildContext context) { |
| final PageStorage? widget = context.findAncestorWidgetOfExactType<PageStorage>(); |
| return widget?.bucket; |
| } |
| |
| @override |
| Widget build(BuildContext context) => child; |
| } |