在规避页面被重建、防止数据丢失的场景中,可能会需要混入AutomaticKeepAliveClientMixin。
最简Demo:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemBuilder: _listItem,
),
),
);
}
Widget _listItem(BuildContext context, int index) {
return const Item();
}
}
class Item extends StatefulWidget {
const Item({super.key});
@override
State<Item> createState() => _ItemState();
}
class _ItemState extends State<Item> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 须调用
return const FlutterLogo(); // 业务中含状态的Widget
}
@override
bool get wantKeepAlive => true; // 保持状态,那么为true
}
得到 wantKeepAlive 为 true 之后,AutomaticKeepAliveClientMixin 中的 _ensureKeepAlive() 方法会被执行,
void _ensureKeepAlive() {
assert(_keepAliveHandle == null);
_keepAliveHandle = KeepAliveHandle();
KeepAliveNotification(_keepAliveHandle!).dispatch(context);
}
这里,构造了 KeepAliveHandle 和 KeepAliveNotification 对象。
KeepAliveNotification 是 Notification 的子类,Notification 是组件间通信的一种方式,其通信方向是由子及父。
那么如果需要完成通信,必然有一个父组件 NotificationListener,从注释可以得知:
/// * [AutomaticKeepAlive], which listens to messages from this mixin.
/// * [KeepAliveNotification], the notifications sent by this mixin.
它是一个 AutomaticKeepAlive 组件 。
在 ListView 组件中,参数 addAutomaticKeepAlives 默认为 true,此时可以容易找到,child 在 执行 build 方法后,又被 AutomaticKeepAlive 包裹。
if (addAutomaticKeepAlives) {
child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}
在 AutomaticKeepAlive 的 build 方法中,返回的是一个 KeepAlive 组件,它可以看作是一个 ParentData (ParentDataWidget 就是给 RenderObject 的 parentData 提供数据的)。
@override
Widget build(BuildContext context) {
return KeepAlive(
keepAlive: _keepingAlive,
child: _child,
);
}
在 AutomaticKeepAlive 的 _updateChild 方法中,找到了前面提到的 NotificationListener。
void _updateChild() {
_child = NotificationListener<KeepAliveNotification>(
onNotification: _addClient,
child: widget.child,
);
}
当子组件被标记为 wantKeepAlive 为 true 后,子组件主动发出通知,父组件收到后,执行 _addClient 方法,实际会走到
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is KeepAliveParentDataMixin);
final KeepAliveParentDataMixin parentData = renderObject.parentData! as KeepAliveParentDataMixin;
if (parentData.keepAlive != keepAlive) {
// No need to redo layout if it became true.
parentData.keepAlive = keepAlive;
final AbstractNodetargetParent = renderObject.parent;
if (targetParent is RenderObject && !keepAlive) {
targetParent.markNeedsLayout();
}
}
}
更新子组件 KeepAlive 的 parentData 的 keepAlive 属性。
这时候,子组件是否 wantKeepAlive 已经确定了,需要看父组件如何处理子组件。继续往上查父组件,这里是ListView。
Viewport 默认有250像素的缓存,建议断点测试时设置 cacheExtent 为 0:
sdk:
/// The default value for the cache extent of the viewport.
///
/// This default assumes [CacheExtentStyle.pixel].
///
/// See also:
///
/// * [RenderViewportBase.cacheExtent] for a definition of the cache extent.
static const double defaultCacheExtent = 250.0;
demo:
cacheExtent: 0,
这样,ListView 滑出屏幕时回收item,滑入时创建item。那么在 performLayout() 流程里,一定会有判断 keepAlive 的逻辑。
默认情况下,ListView.build 构造了 SliverChildBuilderDelegate 对象,传递给 SliverList,容易找到 RenderSliverList 的 performLayout() 中的 collectGarbage 方法:
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
///
/// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is
/// set to true will be removed to a cache instead of being dropped.
///
/// This method also collects any children that were previously kept alive but
/// are now no longer necessary. As such, it should be called every time
/// [performLayout] is run, even if the arguments are both zero.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
_destroyOrCacheChild(firstChild!);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
_destroyOrCacheChild(lastChild!);
trailingGarbage -= 1;
}
_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
return !childParentData.keepAlive;
}).toList().forEach(_childManager.removeChild);
});
}
容易观察到 _keepAliveBucket 这个成员:
/// The nodes being kept alive despite not being visible.
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
void _createOrObtainChild(int index, { required RenderBoxafter }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index)!;
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index!] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
_childManager.removeChild(child);
assert(child.parent == null);
}
}
结合 _createOrObtainChild 和 _destroyOrCacheChild 两个方法:
在 _destroyOrCacheChild 方法中:
如果 keepAlive 为 false ,那么执行 _childManager.removeChild,结果是对应的Element被移除;
如果 keepAlive 为 true ,那么会更新 _keepAliveBucket,不会执行 _childManager.removeChild,RenderObject 对应的 Element 就会保留,那么子组件的 State 也会被保留。
在 _createOrObtainChild 方法中:
_keepAliveBucket 如果含有对应 RenderObject 的话,直接使用,否则新建。
总结:
这里,整个框架可以看作是一个C/S结构:
C端:AutomaticKeepAliveClientMixin,在需要的场景灵活创建
S端:AutomaticKeepAlive,收到客户端数据后处理
通信:KeepAliveNotification,Notification机制
额外的:
1、AutomaticKeepAliveClientMixin 的 build 方法中,会返回一个 _NullWidget,这是一个“占位符”,它的 build 方法虽然抛出异常,但是build方法本身并不会被执行,因为并没有被mount到Widget树上。
它是一个 “Zero cost widget. Use it when you need a placeholder.”可以在源码中看到很多它的这种思想。
2、super.build(context); 一定要调用,保持状态逻辑正确。