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

相关推荐
江上清风山间明月1 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter