1.前置知识
先对
ListView
组件做个测试,这是一个色块列表,其中每个Item
是一个自定义的StatefulWidget
,名为ColorBox
,其中状态量是Checkbox
的选择情况,点击时可切换选中状态
。
色块列表 | 色块列表可选中 |
---|---|
代码语言:javascript复制在
_ColorBoxState#initState
和_ColorBoxState#dispose
回调方法中分别打印信息。
class ColorBox extends StatefulWidget {
final Color color;
final int index;
ColorBox({Key key, this.color, this.index}) : super(key: key);
@override
_ColorBoxState createState() => _ColorBoxState();
}
class _ColorBoxState extends State<ColorBox> {
bool _checked = false;
@override
void initState() {
super.initState();
_checked = false;
print('-----_ColorBoxState#initState---${widget.index}-------');
}
@override
void dispose() {
print('-----_ColorBoxState#dispose---${widget.index}-------');
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
height: 50,
color: widget.color,
child: Row(
children: [
SizedBox(width: 60),
buildCheckbox(),
buildInfo(),
],
),
);
}
Text buildInfo() => Text(
"index ${widget.index}: ${colorString(widget.color)}",
style: TextStyle(color: Colors.white, shadows: [
Shadow(color: Colors.black, offset: Offset(.5, .5), blurRadius: 2)
]),
);
Widget buildCheckbox() => Checkbox(
value: _checked,
onChanged: (v) {
setState(() {
_checked = v;
});
},
);
String colorString(Color color) =>
"#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
}
复制代码
代码语言:javascript复制使用
ListView.builder
构建色块列表。
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage());
}
}
class HomePage extends StatelessWidget {
final List data = [
Colors.purple[50], Colors.purple[100], Colors.purple[200],
Colors.purple[300], Colors.purple[400], Colors.purple[500],
Colors.purple[600], Colors.purple[700], Colors.purple[800],
Colors.purple[900], Colors.red[50], Colors.red[100],
Colors.red[200], Colors.red[300], Colors.red[400],
Colors.red[500], Colors.red[600], Colors.red[700],
Colors.red[800], Colors.red[900],
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
height: 300,
child: ListView.builder(
itemCount: data.length,
itemBuilder: (_, index) => ColorBox(
color: data[index],
index: index,
),
),
),
);
}
}
复制代码
运行后可以发现,屏幕上只显示了 5 个 item ,但是初始化了 10 个,说明
ListView
是会预先初始化
后面一定数目 item 的状态类。通过cacheExtent
可以控制预先加载的数量,比如 item 高 50 ,cacheExtent = 50 *3
就会预加载 3 个。
然后滑动一下列表,看一下 State 方法回调的情况。在
下滑到底
时,可以看到在13
之后0
被 dispose 了,然后前面几个 item 随着滑动被逐步dispose
。 后面上滑到顶
时,前面的 State 又会被逐渐初始化。
下滑到底 | 上滑到顶 |
---|---|
所以一个现象就会呼之欲出:
状态丢失
。
下滑到底 | 上滑到顶 |
---|---|
2. 保持 State 状态
代码语言:javascript复制你可能会发现
ListView
中存在一个addAutomaticKeepAlives
属性,但是用起来似乎没有什么效果,可能很多人都不知道它的真正作用是什么,这个暂且按下不表
。先看如何使State
保持状态。
class _ColorBoxState extends State<ColorBox>
with AutomaticKeepAliveClientMixin { // [1]. with AutomaticKeepAliveClientMixin
bool _checked = false;
@override
bool get wantKeepAlive => true; // [2] 是否保持状态
@override
Widget build(BuildContext context) {
super.build(context); // [3] 在 _ColorBoxState#build 中 调用super.build
复制代码
用法很简单,将
_ColorBoxState with AutomaticKeepAliveClientMixin
,实现抽象方法wantKeepAlive
,返回 true 表示可以保持状态,反正则否。效果如下:
wantKeepAlive:true | wantKeepAlive:false |
---|---|
代码语言:javascript复制是不是感觉很神奇,可能一般的介绍文章到这里就结束了,毕竟已经解决了问题。但可惜,这是在我的 bgm 中。我轻轻地将
addAutomaticKeepAlives 置为 false (默认false)
。 然后,即使_ColorBoxState
的wantKeepAlive 为 true
也无法保持
状态,这就说明addAutomaticKeepAlives
是有作用的。
child: ListView.builder(
addAutomaticKeepAlives: false,
3. List#addAutomaticKeepAlives
做了什么
代码语言:javascript复制下面就来追一下
addAutomaticKeepAlives
是干嘛的。可以看出ListView.builder
中的入参addAutomaticKeepAlives
是 传给SliverChildBuilderDelegate
的。
---->[ListView#builder]----
ListView.builder({
// 略...
bool addAutomaticKeepAlives = true,
// 略...
}) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives, // <--- 入参
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
代码语言:javascript复制在
SliverChildBuilderDelegate
类中的addAutomaticKeepAlives
属性中可以看出,该属性的作用为:是否为每个 child 包裹 AutomaticKeepAlive 组件。
---->[SliverChildBuilderDelegate]----
/// Whether to wrap each child in an [AutomaticKeepAlive].
/// 是否为每个 child 包裹 AutomaticKeepAlive 组件
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
/// 通常,懒加载列表中的 children 被 AutomaticKeepAlive 组件包裹,
/// 以便children可以使用 [KeepAliveNotification] 来保存它们的状态,
/// 否则它们将在屏幕外会被作为垃圾收集。
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
/// 如果子节点要手动维护它们的[KeepAlive]状态,则必须禁用这个特性(和[addRepaintBoundaries])。
/// 如果提前知道所有子节点都不会试图维持自己的生命,禁用此功能可能会更有效。
/// Defaults to true.
final bool addAutomaticKeepAlives;
复制代码
代码语言:javascript复制可以看出,
SliverChildBuilderDelegate#build
中,当addAutomaticKeepAlives=true
时,会把 child 套上一层AutomaticKeepAlive
组件。
---->[SliverChildBuilderDelegate#build]----
@override
Widget build(BuildContext context, int index) {
// 略...
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return KeyedSubtree(child: child, key: key);
}
到这里可以看出
AutomaticKeepAlive
组件是保持 State 的关键之一。所以保持状态并非只是AutomaticKeepAliveClientMixin
的功劳。可以得出AutomaticKeepAliveClientMixin
和AutomaticKeepAlive
一定是故(jian)事(qing)
。
4.AutomaticKeepAliveClientMixin
做了什么
代码语言:javascript复制可以它只能用于
State
的子类之中。在initState
中看出如果wantKeepAlive
为 true,则会执行_ensureKeepAlive
,这也是wantKeepAlive
抽象方法的价值所在。
mixin AutomaticKeepAliveClientMixinextends StatefulWidget> on State {
// 可监听对象
KeepAliveHandle _keepAliveHandle;
@override
void initState() {
super.initState();
if (wantKeepAlive)
_ensureKeepAlive();
}
// 昝略...
}
代码语言:javascript复制其中有一个
KeepAliveHandle
类型的成员变量。KeepAliveHandle
继承自ChangeNotifier
,也就是一个Listenable
可监听对象。通过release
方法来触发事件。注释说,此方法被触发时,就表示该组件不再需要保持状态
了。
class KeepAliveHandle extends ChangeNotifier {
/// Trigger the listeners to indicate that the widget
/// no longer needs to be kept alive.
void release() {
notifyListeners();
}
}
复制代码
代码语言:javascript复制现在来看
_ensureKeepAlive
,实例化KeepAliveHandle
,创建KeepAliveNotification
对象并调用dispatch
方法。
void _ensureKeepAlive() {
assert(_keepAliveHandle == null);
_keepAliveHandle = KeepAliveHandle();
KeepAliveNotification(_keepAliveHandle).dispatch(context);
}
代码语言:javascript复制在
deactivate
中_releaseKeepAlive
。前面看到_keepAliveHandle
执行 release 是,会通知监听者 不再需要保持状态。build
中也是确保在_keepAliveHandle
为 null 时,执行_ensureKeepAlive
,这也是为什么要调用super.build
的原因。
@override
void deactivate() {
if (_keepAliveHandle != null)
_releaseKeepAlive();
super.deactivate();
}
void _releaseKeepAlive() {
_keepAliveHandle.release();
_keepAliveHandle = null;
}
@mustCallSuper
@override
Widget build(BuildContext context) {
if (wantKeepAlive && _keepAliveHandle == null)
_ensureKeepAlive();
return null;
}
这样看来,整个逻辑也并不是非常复杂。最重要的就是创建
KeepAliveNotification
执行dispatch 方法。来看一下源码中对这几个重要类的解释:
AutomaticKeepAlive
监听mixin
发送的信息KeepAliveNotification
由mixin
发送的通知AutomaticKeepAliveClientMixin
很明显,就是用来发送保活信息的客户端(Clinet)
代码语言:javascript复制为了加深理解,我们完全可以把核心逻辑自己写出来。如下,这样操作,即使不混入
AutomaticKeepAliveClientMixin
,也可以实现状态的保持。
class _ColorBoxState extends State<ColorBox> {
bool _checked = false;
KeepAliveHandle _keepAliveHandle;
void _ensureKeepAlive() {
_keepAliveHandle = KeepAliveHandle();
KeepAliveNotification(_keepAliveHandle).dispatch(context);
}
void _releaseKeepAlive() {
if (_keepAliveHandle == null) return;
_keepAliveHandle.release();
_keepAliveHandle = null;
}
@override
void initState() {
super.initState();
_checked = false;
_ensureKeepAlive();
print('-----_ColorBoxState#initState---${widget.index}-------');
}
@override
void deactivate() {
_releaseKeepAlive();
super.deactivate();
}
@override
void dispose() {
print('-----_ColorBoxState#dispose---${widget.index}-------');
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_keepAliveHandle == null)
_ensureKeepAlive();
//略...
复制代码
那
AutomaticKeepAliveClientMixin
存在的意义是什么,当然是方便使用
啦。我们也可以反过来想一想
,如果某个场景围绕着 State 的生命周期有什么固定逻辑,我们也可以仿照这样的方式,使用一个 mixin 为 State 增加某些功能。 很多时候,我们得到了想要的目的
,就不会进一步去探究了,以至于只停留在会了而已
。遇到问题,也只想问出解决方案
。有时再往前踏出一步,你将见到完全不一样的风采
。
5. AutomaticKeepAliveClientMixin
除了 ListView 还能用在哪里?
代码语言:javascript复制
GridView
,和 ListView 一样,内部使用SliverChildBuilderDelegate
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
height: 300,
child: GridView.builder(
gridDelegate:SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1,
crossAxisCount: 2,
),
itemCount: data.length,
itemBuilder: (_, index) => ColorBox(
color: data[index],
index: index,
),
),
),
);
}
由于
GridView
组件是基于SliverGrid
组件实现的,所以SliverGrid
也可以。同理,ListView
组件基于SliverFixedExtentList
或SliverList
组件实现的,它们也可以。
代码语言:javascript复制
PageView
也使用了SliverChildBuilderDelegate
,所以也具有相关特性。不过没有对外界暴露设置addAutomaticKeepAlives
的途径,永远为true。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
height: 300,
child: PageView.builder(
itemCount: data.length,
itemBuilder: (_, index) => ColorBox(
color: data[index],
index: index,
),
),
),
);
}
代码语言:javascript复制
TabBarView
组件内部基于PageView
实现,所以也适用。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: DefaultTabController(
length: data.length,
child: Column(
children: [
_buildTabBar(),
Container(
color: Colors.purple,
width: MediaQuery.of(context).size.width,
height: 200,
child: _buildTableBarView())
],
),
),
);
}
Widget _buildTabBar() => TabBar(
onTap: (tab) => print(tab),
labelStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
unselectedLabelStyle: TextStyle(fontSize: 16),
isScrollable: true,
labelColor: Colors.blue,
indicatorWeight: 3,
indicatorPadding: EdgeInsets.symmetric(horizontal: 10),
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.orangeAccent,
tabs: data.map((e) => Tab(text: colorString(e))).toList(),
);
Widget _buildTableBarView() => TabBarView(
children: data
.map((e) => Center(
child: ColorBox(
color: e,
index: data.indexOf(e),
)))
.toList());
String colorString(Color color) =>
"#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
复制代码
这些就是常用的有保持状态需求的组件, 至于什么时候需要进行状态的保存,我只能说:
当你饿了,你自然会知道什么时候想吃饭
。
@张风捷特烈 2020.12.18 未允禁转
我的公众号:编程之王
联系我----
~ END ~