前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【-Flutter 探索-】AutomaticKeepAliveClientMixin 保持 State 状态

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

作者头像
张风捷特烈
发布2020-12-21 09:39:30
2.1K0
发布2020-12-21 09:39:30
举报
文章被收录于专栏:Android知识点总结
1.前置知识

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

色块列表

色块列表可选中

色块列表
色块列表
色块列表可选中
色块列表可选中

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

代码语言:javascript
复制
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 构建色块列表。

代码语言:javascript
复制
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 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 个。


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

下滑到底

上滑到顶

下滑到底
下滑到底
上滑到顶
上滑到顶

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

下滑到底

上滑到顶

下滑到底
下滑到底
上滑到顶
上滑到顶
2. 保持 State 状态

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

代码语言:javascript
复制
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:true

wantKeepAlive:false

wantKeepAlive:true
wantKeepAlive:true
wantKeepAlive:false
wantKeepAlive:false

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

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

3. List#addAutomaticKeepAlives 做了什么

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

代码语言:javascript
复制
---->[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 组件。

代码语言:javascript
复制
---->[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 组件。

代码语言:javascript
复制
---->[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 抽象方法的价值所在。

代码语言:javascript
复制
mixin AutomaticKeepAliveClientMixinextends StatefulWidget> on State {
  // 可监听对象 
  KeepAliveHandle _keepAliveHandle;

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

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

代码语言:javascript
复制
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 方法。

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

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

代码语言:javascript
复制
@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)

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

代码语言:javascript
复制
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

代码语言:javascript
复制
@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。

代码语言:javascript
复制
@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 实现,所以也适用。

代码语言:javascript
复制
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: DefaultTabController(
      length: data.length,
      child: Column(
        children: [
          _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 未允禁转 我的公众号:编程之王 联系我---- ~ END ~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.前置知识
  • 2. 保持 State 状态
  • 3. List#addAutomaticKeepAlives 做了什么
  • 4.AutomaticKeepAliveClientMixin 做了什么
  • 5. AutomaticKeepAliveClientMixin 除了 ListView 还能用在哪里?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档