阅读 1194

【-Flutter 探索-】AutomaticKeepAliveClientMixin 保持 State 状态

1.前置知识

先对 ListView 组件做个测试,这是一个色块列表,其中每个 Item 是一个自定义的 StatefulWidget ,名为 ColorBox ,其中状态量是 Checkbox 的选择情况,点击时可切换选中状态

色块列表色块列表可选中
image-20201218125140487image-20201218125114851

_ColorBoxState#initState_ColorBoxState#dispose 回调方法中分别打印信息。

class ColorBox extends StatefulWidget {
  final Color color;
  final int index;

  ColorBox({Key key, this.color, this.index}) : super(key: key);

  @override
  _ColorBoxState createState() => _ColorBoxState();
}

class _ColorBoxState extends State<ColorBox> {
  bool _checked = false;

  @override
  void initState() {
    super.initState();
    _checked = false;
    print('-----_ColorBoxState#initState---${widget.index}-------');
  }

  @override
  void dispose() {
    print('-----_ColorBoxState#dispose---${widget.index}-------');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Container(
      alignment: Alignment.center,
      height: 50,
      color: widget.color,
      child: Row(
        children: [
          SizedBox(width: 60),
          buildCheckbox(),
          buildInfo(),
        ],
      ),
    );
  }

  Text buildInfo() => Text(
          "index ${widget.index}: ${colorString(widget.color)}",
          style: TextStyle(color: Colors.white, shadows: [
            Shadow(color: Colors.black, offset: Offset(.5, .5), blurRadius: 2)
          ]),
        );

  Widget buildCheckbox() => Checkbox(
          value: _checked,
          onChanged: (v) {
            setState(() {
              _checked = v;
            });
          },
        );

  String colorString(Color color) =>
      "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
}
复制代码

使用 ListView.builder 构建色块列表。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomePage());
  }
}

class HomePage extends StatelessWidget {
  final List<Color> data = [
    Colors.purple[50], Colors.purple[100],  Colors.purple[200],
    Colors.purple[300],   Colors.purple[400],  Colors.purple[500],
    Colors.purple[600],  Colors.purple[700], Colors.purple[800],
    Colors.purple[900],  Colors.red[50],  Colors.red[100],
    Colors.red[200], Colors.red[300],  Colors.red[400],
    Colors.red[500], Colors.red[600],  Colors.red[700],
    Colors.red[800],  Colors.red[900],
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        height: 300,
        child: ListView.builder(
          itemCount: data.length,
          itemBuilder: (_, index) => ColorBox(
            color: data[index],
            index: index,
          ),
        ),
      ),
    );
  }
}
复制代码

运行后可以发现,屏幕上只显示了 5 个 item ,但是初始化了 10 个,说明 ListView 是会预先初始化后面一定数目 item 的状态类。通过 cacheExtent 可以控制预先加载的数量,比如 item 高 50 ,cacheExtent = 50 *3 就会预加载 3 个。

image-20201218125933074


然后滑动一下列表,看一下 State 方法回调的情况。在下滑到底时,可以看到在 13 之后 0 被 dispose 了,然后前面几个 item 随着滑动被逐步 dispose。 后面 上滑到顶 时,前面的 State 又会被逐渐初始化。

下滑到底上滑到顶
image-20201218130658359image-20201218130721619

所以一个现象就会呼之欲出: 状态丢失

下滑到底上滑到顶
滑动状态丢失

2. 保持 State 状态

你可能会发现 ListView 中存在一个 addAutomaticKeepAlives 属性,但是用起来似乎没有什么效果,可能很多人都不知道它的真正作用是什么,这个暂且按下不表。先看如何使 State 保持状态。

class _ColorBoxState extends State<ColorBox>
    with AutomaticKeepAliveClientMixin { // [1]. with AutomaticKeepAliveClientMixin
  bool _checked = false;

  @override
  bool get wantKeepAlive => true; // [2] 是否保持状态
  
    @override
  Widget build(BuildContext context) {
    super.build(context); // [3] 在 _ColorBoxState#build 中 调用super.build
复制代码

用法很简单,将 _ColorBoxState with AutomaticKeepAliveClientMixin ,实现抽象方法 wantKeepAlive,返回 true 表示可以保持状态,反正则否。效果如下:

wantKeepAlive:truewantKeepAlive:false
保持状态状态丢失

是不是感觉很神奇,可能一般的介绍文章到这里就结束了,毕竟已经解决了问题。但可惜,这是在我的 bgm 中。我轻轻地将 addAutomaticKeepAlives 置为 false (默认true) 。 然后,即使 _ColorBoxStatewantKeepAlive 为 true无法保持状态,这就说明 addAutomaticKeepAlives 是有作用的。

child: ListView.builder(
    addAutomaticKeepAlives: false,
复制代码

3. List#addAutomaticKeepAlives 做了什么

下面就来追一下 addAutomaticKeepAlives 是干嘛的。可以看出ListView.builder 中的入参 addAutomaticKeepAlives 是 传给 SliverChildBuilderDelegate 的。

---->[ListView#builder]----
 ListView.builder({
    // 略...
    bool addAutomaticKeepAlives = true,
    // 略...
  }) : assert(itemCount == null || itemCount >= 0),
       assert(semanticChildCount == null || semanticChildCount <= itemCount),
       childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         childCount: itemCount,
         addAutomaticKeepAlives: addAutomaticKeepAlives, // <--- 入参
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
复制代码

SliverChildBuilderDelegate 类中的 addAutomaticKeepAlives 属性中可以看出,该属性的作用为: 是否为每个 child 包裹 AutomaticKeepAlive 组件。

---->[SliverChildBuilderDelegate]----
/// Whether to wrap each child in an [AutomaticKeepAlive].
/// 是否为每个 child 包裹 AutomaticKeepAlive 组件
  
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
  
/// 通常,懒加载列表中的 children 被 AutomaticKeepAlive 组件包裹,
/// 以便children可以使用 [KeepAliveNotification] 来保存它们的状态,
/// 否则它们将在屏幕外会被作为垃圾收集。
  
/// 
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
  
/// 如果子节点要手动维护它们的[KeepAlive]状态,则必须禁用这个特性(和[addRepaintBoundaries])。
/// 如果提前知道所有子节点都不会试图维持自己的生命,禁用此功能可能会更有效。
  
/// Defaults to true.
final bool addAutomaticKeepAlives;
复制代码

可以看出,SliverChildBuilderDelegate#build 中,当 addAutomaticKeepAlives=true 时,会把 child 套上一层 AutomaticKeepAlive 组件。

---->[SliverChildBuilderDelegate#build]----
 @override
 Widget build(BuildContext context, int index) {
 	// 略...
   if (addAutomaticKeepAlives)
     child = AutomaticKeepAlive(child: child);
   return KeyedSubtree(child: child, key: key);
 }
复制代码

到这里可以看出 AutomaticKeepAlive 组件是保持 State 的关键之一。所以保持状态并非只是 AutomaticKeepAliveClientMixin 的功劳。可以得出 AutomaticKeepAliveClientMixinAutomaticKeepAlive 一定是 故(jian)事(qing)


4.AutomaticKeepAliveClientMixin 做了什么

可以它只能用于 State 的子类之中。在 initState 中看出如果 wantKeepAlive 为 true,则会执行 _ensureKeepAlive,这也是 wantKeepAlive 抽象方法的价值所在。

mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
  // 可监听对象 
  KeepAliveHandle _keepAliveHandle;

  @override
  void initState() {
    super.initState();
    if (wantKeepAlive)
      _ensureKeepAlive();
  }
 // 昝略...
}
复制代码

其中有一个 KeepAliveHandle 类型的成员变量。KeepAliveHandle 继承自 ChangeNotifier,也就是一个 Listenable 可监听对象。通过 release 方法来触发事件。注释说,此方法被触发时,就表示该组件不再需要保持状态了。

class KeepAliveHandle extends ChangeNotifier {
  /// Trigger the listeners to indicate that the widget
  /// no longer needs to be kept alive.
  void release() {
    notifyListeners();
  }
}
复制代码

现在来看 _ensureKeepAlive,实例化 KeepAliveHandle ,创建 KeepAliveNotification 对象并调用 dispatch 方法。

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

deactivate_releaseKeepAlive 。前面看到 _keepAliveHandle执行 release 是,会通知监听者 不再需要保持状态。build 中也是确保在 _keepAliveHandle 为 null 时,执行 _ensureKeepAlive,这也是为什么要调用 super.build 的原因。

@override
void deactivate() {
  if (_keepAliveHandle != null)
    _releaseKeepAlive();
  super.deactivate();
}

void _releaseKeepAlive() {
  _keepAliveHandle.release();
  _keepAliveHandle = null;
}

@mustCallSuper
@override
Widget build(BuildContext context) {
  if (wantKeepAlive && _keepAliveHandle == null)
    _ensureKeepAlive();
  return null;
}
复制代码

这样看来,整个逻辑也并不是非常复杂。最重要的就是创建 KeepAliveNotification执行dispatch 方法。来看一下源码中对这几个重要类的解释:

  • AutomaticKeepAlive 监听 mixin 发送的信息
  • KeepAliveNotificationmixin 发送的通知
  • AutomaticKeepAliveClientMixin 很明显,就是用来发送保活信息的 客户端(Clinet)

image-20201218185125940

为了加深理解,我们完全可以把核心逻辑自己写出来。如下,这样操作,即使不混入 AutomaticKeepAliveClientMixin,也可以实现状态的保持。

class _ColorBoxState extends State<ColorBox> {
  bool _checked = false;

  KeepAliveHandle _keepAliveHandle;

  void _ensureKeepAlive() {
    _keepAliveHandle = KeepAliveHandle();
    KeepAliveNotification(_keepAliveHandle).dispatch(context);
  }

  void _releaseKeepAlive() {
    if (_keepAliveHandle == null) return;
    _keepAliveHandle.release();
    _keepAliveHandle = null;
  }

  @override
  void initState() {

    super.initState();
    _checked = false;
    _ensureKeepAlive();
    print('-----_ColorBoxState#initState---${widget.index}-------');
  }


  @override
  void deactivate() {
    _releaseKeepAlive();
    super.deactivate();
  }

  @override
  void dispose() {
    print('-----_ColorBoxState#dispose---${widget.index}-------');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_keepAliveHandle == null)
      _ensureKeepAlive();
   //略...
复制代码

AutomaticKeepAliveClientMixin 存在的意义是什么,当然是方便使用啦。我们也可以反过来想一想,如果某个场景围绕着 State 的生命周期有什么固定逻辑,我们也可以仿照这样的方式,使用一个 mixin 为 State 增加某些功能。 很多时候,我们得到了想要的目的,就不会进一步去探究了,以至于只停留在会了而已。遇到问题,也只想问出解决方案。有时再往前踏出一步,你将见到完全不一样的风采


5. AutomaticKeepAliveClientMixin 除了 ListView 还能用在哪里?

GridView,和 ListView 一样,内部使用 SliverChildBuilderDelegate

image-20201218195650440

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: Container(
      height: 300,
      child: GridView.builder(
        gridDelegate:SliverGridDelegateWithFixedCrossAxisCount(
          childAspectRatio: 1,
          crossAxisCount: 2,
        ),
        itemCount: data.length,
        itemBuilder: (_, index) => ColorBox(
            color: data[index],
            index: index,
          ),
        ),
    ),
  );
}
复制代码

由于 GridView 组件是基于 SliverGrid 组件实现的,所以 SliverGrid 也可以。同理, ListView 组件基于 SliverFixedExtentListSliverList 组件实现的,它们也可以。


PageView 也使用了 SliverChildBuilderDelegate ,所以也具有相关特性。不过没有对外界暴露设置addAutomaticKeepAlives 的途径,永远为true。

image-20201218194906862

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: Container(
      height: 300,
      child: PageView.builder(
        itemCount: data.length,
        itemBuilder: (_, index) => ColorBox(
            color: data[index],
            index: index,
          ),
        ),
    ),
  );
}
复制代码

TabBarView 组件内部基于 PageView 实现,所以也适用。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: DefaultTabController(
      length: data.length,
      child: Column(
        children: <Widget>[
          _buildTabBar(),
          Container(
              color: Colors.purple,
              width: MediaQuery.of(context).size.width,
              height: 200,
              child: _buildTableBarView())
        ],
      ),
    ),
  );
}

Widget _buildTabBar() => TabBar(
      onTap: (tab) => print(tab),
      labelStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      unselectedLabelStyle: TextStyle(fontSize: 16),
      isScrollable: true,
      labelColor: Colors.blue,
      indicatorWeight: 3,
      indicatorPadding: EdgeInsets.symmetric(horizontal: 10),
      unselectedLabelColor: Colors.grey,
      indicatorColor: Colors.orangeAccent,
      tabs: data.map((e) => Tab(text: colorString(e))).toList(),
    );
Widget _buildTableBarView() => TabBarView(
    children: data
        .map((e) => Center(
                child: ColorBox(
              color: e,
              index: data.indexOf(e),
            )))
        .toList());

String colorString(Color color) =>
    "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
复制代码

这些就是常用的有保持状态需求的组件, 至于什么时候需要进行状态的保存,我只能说:当你饿了,你自然会知道什么时候想吃饭

@张风捷特烈 2020.12.18 未允禁转 我的公众号:编程之王 联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328 ~ END ~