// 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(MyApp()); /// ``` /// /// ```dart /// class MyApp extends StatelessWidget { /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// home: MyHomePage(), /// ); /// } /// } /// /// class MyHomePage extends StatefulWidget { /// @override /// _MyHomePageState createState() => _MyHomePageState(); /// } /// /// class _MyHomePageState extends State<MyHomePage> { /// final List<Widget> pages = <Widget>[ /// ColorBoxPage( /// key: PageStorageKey('pageOne'), /// ), /// ColorBoxPage( /// key: PageStorageKey('pageTwo'), /// ) /// ]; /// int currentTab = 0; /// final PageStorageBucket _bucket = PageStorageBucket(); /// /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: Text("Persistence Example"), /// ), /// body: PageStorage( /// child: pages[currentTab], /// bucket: _bucket, /// ), /// bottomNavigationBar: BottomNavigationBar( /// currentIndex: currentTab, /// onTap: (int index) { /// setState(() { /// currentTab = index; /// }); /// }, /// items: <BottomNavigationBarItem>[ /// BottomNavigationBarItem( /// icon: Icon(Icons.home), /// label: 'page 1', /// ), /// BottomNavigationBarItem( /// icon: Icon(Icons.settings), /// label: 'page2', /// ), /// ], /// ), /// ); /// } /// } /// /// class ColorBoxPage extends StatelessWidget { /// ColorBoxPage({ /// Key? key, /// }) : super(key: key); /// /// @override /// Widget build(BuildContext context) { /// return ListView.builder( /// itemExtent: 250.0, /// itemBuilder: (context, index) => Container( /// padding: EdgeInsets.all(10.0), /// child: Material( /// color: index % 2 == 0 ? 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; }