讲清楚 AutomaticKeepAliveClientMixin

在规避页面被重建、防止数据丢失的场景中,可能会需要混入AutomaticKeepAliveClientMixin。

最简Demo:

scala 复制代码
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() 方法会被执行,

scss 复制代码
void _ensureKeepAlive() {
  assert(_keepAliveHandle == null);
  _keepAliveHandle = KeepAliveHandle();
  KeepAliveNotification(_keepAliveHandle!).dispatch(context);
}

这里,构造了 KeepAliveHandle 和 KeepAliveNotification 对象。

KeepAliveNotification 是 Notification 的子类,Notification 是组件间通信的一种方式,其通信方向是由子及父。

那么如果需要完成通信,必然有一个父组件 NotificationListener,从注释可以得知:

arduino 复制代码
 /// * [AutomaticKeepAlive], which listens to messages from this mixin.
/// * [KeepAliveNotification], the notifications sent by this mixin. 

它是一个 AutomaticKeepAlive 组件 。

在 ListView 组件中,参数 addAutomaticKeepAlives 默认为 true,此时可以容易找到,child 在 执行 build 方法后,又被 AutomaticKeepAlive 包裹。

ini 复制代码
if (addAutomaticKeepAlives) {
  child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}

在 AutomaticKeepAlive 的 build 方法中,返回的是一个 KeepAlive 组件,它可以看作是一个 ParentData (ParentDataWidget 就是给 RenderObject 的 parentData 提供数据的)。

less 复制代码
@override
Widget build(BuildContext context) {
  return KeepAlive(
    keepAlive: _keepingAlive,
    child: _child,
  );
}

在 AutomaticKeepAlive 的 _updateChild 方法中,找到了前面提到的 NotificationListener。

ini 复制代码
void _updateChild() {
  _child = NotificationListener<KeepAliveNotification>(
    onNotification: _addClient,
    child: widget.child,
  );
}

当子组件被标记为 wantKeepAlive 为 true 后,子组件主动发出通知,父组件收到后,执行 _addClient 方法,实际会走到

ini 复制代码
@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 AbstractNode? targetParent = renderObject.parent;
    if (targetParent is RenderObject && !keepAlive) {
      targetParent.markNeedsLayout();
    }
  }
}

更新子组件 KeepAlive 的 parentData 的 keepAlive 属性。

这时候,子组件是否 wantKeepAlive 已经确定了,需要看父组件如何处理子组件。继续往上查父组件,这里是ListView。

Viewport 默认有250像素的缓存,建议断点测试时设置 cacheExtent 为 0:

arduino 复制代码
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 方法:

scss 复制代码
 /// 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 这个成员:

arduino 复制代码
 /// The nodes being kept alive despite not being visible.
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
ini 复制代码
void _createOrObtainChild(int index, { required RenderBox? after }) {
  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); 一定要调用,保持状态逻辑正确。

相关推荐
Georgewu9 天前
【HarmonyOS 5】鸿蒙跨平台开发方案详解(一)
flutter·harmonyos
爱吃鱼的锅包肉9 天前
Flutter开发中记录一个非常好用的图片缓存清理的插件
flutter
张风捷特烈10 天前
每日一题 Flutter#13 | build 回调的 BuildContext 是什么
android·flutter·面试
恋猫de小郭10 天前
Flutter 又双叒叕可以在 iOS 26 的真机上 hotload 运行了,来看看又是什么黑科技
android·前端·flutter
QC七哥10 天前
跨平台开发flutter初体验
android·flutter·安卓·桌面开发
小喷友10 天前
Flutter 从入门到精通(水)
前端·flutter·app
恋猫de小郭10 天前
Flutter 里的像素对齐问题,深入理解为什么界面有时候会出现诡异的细线?
android·前端·flutter
tbit11 天前
dart私有命名构造函数的作用与使用场景
flutter·dart
法的空间11 天前
JsonToDart,你已经是一个成熟的工具了,接下来就靠你自己继续进化了!
android·flutter·ios