讲清楚 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); 一定要调用,保持状态逻辑正确。

相关推荐
大龄秃头程序员2 小时前
Riverpod 深度解析:从响应式本质到最小可运行闭环
flutter
曹小强wxx3 小时前
让 AI 写代码不“翻车”:用 Claude Code + Codex 给 Flutter 项目搭一套工程规范
flutter·ai编程·claude
西西学代码6 小时前
像素和颜色之间的转换(Flutter)
前端·javascript·flutter
曹小强wxx7 小时前
Flutter 局域网设备发现:UDP 广播 + mDNS 双路合并,以及靠谱的在线判定与断线重连
物联网·网络协议·flutter
FungLeo9 小时前
成为全栈·Flutter App 篇·移动端错误与限流:429、重试和写请求边界
flutter·限流·dart·dio·错误处理·成为全栈
恋猫de小郭9 小时前
Kotlin Toolchain 0.13,JetBrains 推荐 KMP 都切换到到全新工具链
android·前端·flutter
m0_738185829 小时前
Flutter 鸿蒙化实战:rive 适配 OpenHarmony,Rive 动画
flutter·华为·harmonyos·鸿蒙
Geek_Vison21 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo21 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185821 天前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙