开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 32 天,点击查看活动详情
可滚动组件子项缓存
之前使用的ListView和GridView组件中,都有一个AddAutomaticKeepAlives属性,,如果AddAutomatocKeepAlive为true时,则ListView会为每一个列表添加一个AutomaticKeepAlive父组件。PageView的默认构造函数和PageView.builder构造函数中没有该参数,但他们最终会生成一个SliverChildDelegate来负责列表项的按需加载,而在SliverChildDelegate中,每当列表项构建完成后,SliverChild Delegate都会为其添加一个AutomaticKeepAlive父组件。
AutomaticKeepAlive
AutommaticKeepAlive的组件主要作用是将列表的根RenderObject的keepAlive按需自动标记为true或false。暂且认为RenderObject对应的组件是列表项的根Widget,代表整个列表项组件,同时我们将列表组件的Viewport区域+cacheExtent(预渲染区域)称为加载区域。
- 当keepAlive标记为false时,如果列表项滑出加载区域时,列表组件将会被销毁。
- 当keepAlive标记为true时,当列表项滑出加载区域后,Viewport会将列表组件缓存起来,当列表项进入加载区域时,Viewport优先从缓存中查找是否已缓存,如果有则直接复用,如果没有则重新创建列表项。
AutomaticKeepAlive类似于一个Server,它的子组件可以是Client,子组件想改变是否需要缓存的状态时,需要向AutomaticKeepAlive发一个通知消息(KeepAliveNotification),Automatic收到后会去变更keepAlive的状态,如果有必要会同时做一些资源清理工作(比如释放缓存)。
之前提到过,想让PageView页面实现缓存,让Page页变成一个AutomaticKeepAlive Client即可。Flutter中提供了一个AutomaticKeepAlive ClientMixin,我们只需要让PageState混入这个Mixin,同时添加一些必要操作即可。
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要缓存
}
提供一个wantKeepAlive参数,表示AutomaticKeepAlive是否需要缓存当前列表项。另外我们必须在build方法中调用一下super.build(context),该方法实现AutomaticKeepAliveClientMixin中,功能就是根据当前的wantKeepAlive的值给AutomaticKeepAlive发送消息,Automatic收到消息后就会对列表项进行标记。 流程图:
此时运行实例会发现PageView只会创建一次,表明缓存成功了。
注意:如果采用PageView.custom构建页面时,没有给列表包装AutomaticKeepAlive父组件,则上述方案不能正常工作,因为此时Client发出消息后,找不到Server。
KeepAliveWrapper
通过AutomaticKeepAliveClientMixin快速的实现了页面缓存功能,但是通过混入的方式实现不是很优雅。因为必须改Page的代码,有侵入性,使用会不灵活,为此,可以封装一个KeepAliveWrapper,如果那个组件需要缓存,只需要使用KeepAliveWrapper包裹以下即可。
@override
Widget build(BuildContext context) {
var children = <Widget>[];
for (int i = 0; i < 6; ++i) {
//只需要用 KeepAliveWrapper 包装一下即可
children.add(KeepAliveWrapper(child:Page( text: '$i'));
}
return PageView(children: children);
}
实现源码:
class KeepAliveWrapper extends StatefulWidget {
const KeepAliveWrapper({
Key? key,
this.keepAlive = true,
required this.child,
}) : super(key: key);
final bool keepAlive;//控制是否需要缓存
final Widget child;//传入的widget
@override
_KeepAliveWrapperState createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;//将传入的Widget返回,只是中间加了一些属性,类似于包装。
}
@override
void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
if(oldWidget.keepAlive != widget.keepAlive) {
// keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中
updateKeepAlive();
}
super.didUpdateWidget(oldWidget);
}
//重新系统属性,获取控制变量
@override
bool get wantKeepAlive => widget.keepAlive;
}
使用实例:
class KeepAliveTest extends StatelessWidget {
const KeepAliveTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: (_, index) {
return KeepAliveWrapper(
// 为 true 后会缓存所有的列表项,列表项将不会销毁。
// 为 false 时,列表项滑出预加载区域后将会别销毁。
// 使用时一定要注意是否必要,因为对所有列表项都缓存的会导致更多的内存消耗
keepAlive: true,
child: ListItem(index: index),
);
});
}
}
class ListItem extends StatefulWidget {
const ListItem({Key? key, required this.index}) : super(key: key);
final int index;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(title: Text('${widget.index}'));
}
@override
void dispose() {
print('dispose ${widget.index}');
super.dispose();
}
}
可以分别设置keepAlive为ture和false看下运行效果,会发现,keepAlive为True时基本无打印,也就是缓存成功,未释放,为false时,则会打印,表示已释放。缓存有利有弊,还是需要看使用场景。开发还是需要本着勤俭节约的原则。