【Flutter 知识集锦】从 restorationId 来说临时状态存储

4,226 阅读2分钟

1、缘起

如果我不提 restorationId 属性,可能绝大多数人都不知道他是干嘛的,甚至连它的存在都不知道。即便它在组件作为参中出现的频率挺高。下面先看一下有该属性的一些组件,比如:在 ListView 中有 restorationId 的属性。


GridView 中也有 restorationId 的属性。


PageView 组件中也有 restorationId 的属性。


SingleChildScrollView 组件中也有 restorationId 的属性。


NestedScrollView 组件中也有 restorationId 的属性。


CustomScrollView 组件中也有 restorationId 的属性。


TextField 组件中也有一个 restorationId 的属性。

除此之外还有很多其他的组件有 restorationId 属性,可以感觉到只要和 滑动沾点边的,好像都有 restorationId 的属性。说了这么多,下面我们先来看一下这个属性的作用。


2. restorationId 属性的作用

下面以 ListView 为例,介绍一下 restorationId 属性的作用。如下两个动图分别是 无 restorationId有 restorationId 的效果。可见 restorationId 的作用是在某种情况下,保持滑动的偏移量

无 restorationId有 restorationId
class ListViewDemo extends StatelessWidget {
  const ListViewDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView(
      restorationId: 'toly', //tag1
        children: List.generate(25, (index) => ItemBox(index: index,)).toList());
  }
}

class ItemBox extends StatelessWidget {
  final int index;

  const ItemBox({Key? key, required this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      decoration: BoxDecoration(
          border: Border(
              bottom: BorderSide(
        width: 1 / window.devicePixelRatio,
      ))),
      height: 56,
      child: Text(
        '第 $index 个',
        style: TextStyle(fontSize: 20),
      ),
    );
  }
}

另外,说明一点,为例方便演示恢复的触发,需要在 开发者选项 中勾选 不保留活动 ,其作用是用户离开后会杀掉 Activity 。比如点击Home键、菜单栏切换界面时,Activity 并不为立即销毁,而是系统视情况而定。打开这个选项可以避免测试的不确定因素。注意:测试后,一定要关掉

在 Android 中,是通过 onSaveInstanceState 进行实现的。 当系统"未经你许可" 时销毁了你的 Activity 时,比如横竖屏切换、点击 Home 键、导航菜单栏切换。系统会提供一个机会让通过 onSaveInstanceState 回调来你保存临时状态数据,这样可以保证下次用户进入时产生违和感
另外有一点非常重要,这里并不是将状态永久存储,当用户主动退出应用,是不会触发 onSaveInstanceState 的。也就是说,如果你一个 ListView 设置了 restorationId ,用户滑了一下后,按返回键退出,那么再进来时不会还原到原位置。注意,要是其生效需要在 MaterialApp 中为 restorationScopeId 指定任意字符串。


3.如何通过 restoration 机制存储其他数据

到这里可能很多人就已满足了,原来 restorationId 可以存储临时状态,新技能 get 。但这只是冰山一角, restorationId 是被封装在 ListView 中,只能存储滑动偏移量,这还有值得举一反三,继续深挖的东西。下面通过官方给的一个计时器小demo,认识一下 RestorationMixin

普通计数器状态存储计数器

上面两个动态表现出通过 状态存储 的计时器可以在用户主动退出应用时,存储状态数据,进入时保持状态。其中的关键在于 RestorationMixin 。普通的计时器源码就不贴了,大家应该已经烂熟于心了。实现定义一个 RestorableCounter 组件用于界面展示,

void main() => runApp(const RestorationExampleApp());

class RestorationExampleApp extends StatelessWidget {
  const RestorationExampleApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      restorationScopeId: 'app',
      title: 'Restorable Counter',
      home: RestorableCounter(restorationId: 'counter'),
    );
  }
}

class RestorableCounter extends StatefulWidget {
  const RestorableCounter({Key? key, this.restorationId}) : super(key: key);
  final String? restorationId;
  @override
  State<RestorableCounter> createState() => _RestorableCounterState();
}

如下在 _RestorableCounterState 中进行操作:首先混入 RestorationMixin ,然后覆写 restorationIdrestoreState 方法。提供 RestorableInt 对象记录数值 。

class _RestorableCounterState extends State<RestorableCounter>
    with RestorationMixin{ // 1. 混入 RestorationMixin

  // 3. 使用 RestorableInt 对象记录数值
  final RestorableInt _counter = RestorableInt(0);


  // 2. 覆写 restorationId 提供 id 
  // @override
  String? get restorationId => widget.restorationId;


  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    // 4. 注册 _counter
    registerForRestoration(_counter, 'count');
  }


  @override
  void dispose() {
    _counter.dispose(); // 5. 销毁
    super.dispose();
  }

在组件构建中,我们可以通过 _counter.value访问或操作数值。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Restorable Counter'),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('You have pushed the button this many times:'),
          Text(
            '${_counter.value}',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    ),
  );
}

void _incrementCounter() {
  setState(() {
    _counter.value++;
  });
}

刚才有的是 RestorableInt,可能有人担心别的数据类型怎么办。Flutter 中提供了很多 RestorableXXX 的数据类型以供使用。如果不够用,可以通过拓展 RestorableProperty<T> 来自定义 RestorableXXX 完成需求。

从官方的更新公告上可以看出,目前暂不支持 iOS ,不过在以后会进行支持。


4. 滑动体系中的状态存储是如何实现的

当看完上面的小 demo,你可能会比较好奇,滑动体系中是如何存储的,下面我们就来看看吧。我们追随 ListViewrestorationId 属性踪迹,可以看到它会一路向父级构造中传递。最终在 ScrollView 中作为 Scrollable 组件的入参使用。
也就是说,这个属性的根源是用于 Scrollable 中的。而这个组件是滑动触发的根基,这也是为什么滑动相关的组件都有 restorationId 属性的原因。

ListView --> BoxScrollView --> ScrollView --> Scrollable


ScrollableState 混入了 RestorationMixin ,其中用于存储的类型为 _RestorableScrollOffset

同样覆写了 restoreStaterestorationId 方法。


这时再看 TextField 组件的实现也是类似,也就说明 TextField 组件也具有这种恢复状态的特性。

那本文就到这里,更深层的 RestorationMixin 实现,以及其相关的其他类,还待继续研究,敬请期待。