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

相关推荐
传奇开心果编程28 分钟前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘3 小时前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra425 小时前
不想开会员,我写了一个记账 App
flutter
2601_949575869 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间9 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭11 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇1 天前
Dart 之模式
android·flutter·dart
淡淡的香烟1 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端