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
的作用是在某种情况下,保持滑动的偏移量
。
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
中,只能存储滑动偏移量,这还有值得举一反三,继续深挖的东西。
上面两个动态表现出通过 状态存储
的计时器可以在用户主动
退出应用时,存储状态数据,进入时保持状态。其中的关键在于 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
,然后覆写 restorationId
和 restoreState
方法。提供 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
,你可能会比较好奇,滑动体系中是如何存储的,下面我们就来看看吧。我们追随 ListView
的 restorationId
属性踪迹,可以看到它会一路向父级构造中传递。最终在 ScrollView
中作为 Scrollable
组件的入参使用。
也就是说,这个属性的根源是用于 Scrollable
中的。而这个组件是滑动触发的根基,这也是为什么滑动相关的组件都有 restorationId
属性的原因。
ListView --> BoxScrollView --> ScrollView --> Scrollable
ScrollableState
混入了 RestorationMixin
,其中用于存储的类型为 _RestorableScrollOffset
。
同样覆写了 restoreState
和 restorationId
方法。
这时再看 TextField
组件的实现也是类似,也就说明 TextField
组件也具有这种恢复状态的特性。
那本文就到这里,更深层的 RestorationMixin
实现,以及其相关的其他类,还待继续研究,敬请期待。