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

相关推荐
迷雾漫步者36 分钟前
Flutter组件————PageView
flutter·跨平台·dart
迷雾漫步者8 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
coder_pig13 小时前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
捡芝麻丢西瓜15 小时前
flutter自学笔记5- dart 编码规范
flutter·dart
恋猫de小郭15 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
sunly_2 天前
Flutter:导航,tab切换,顶部固定,列表分页滚动
开发语言·javascript·flutter
敲代码的小强2 天前
Flutter项目兼容鸿蒙Next系统
flutter·华为·harmonyos
Zh-jie2 天前
flutter 快速实现侧边栏
前端·javascript·flutter
truemi.733 天前
flutter --no-color pub get 超时解决方法
android·flutter
王家视频教程图书馆3 天前
flutter 使用dio 请求go语言后台数据接口展示瀑布流图片
flutter