Flutter小技巧|关于Key与树你所该知道的(图解)

·  阅读 414
Flutter小技巧|关于Key与树你所该知道的(图解)

Hi 👋

我的个人项目扫雷Elic 无尽天梯梦见账本隐私访问记录
类型游戏财务工具
AppStoreElicUmemi隐私访问记录

更多专栏:

Lawliet的独立开发碎碎念

Lawliet的iOS游园会

Lawliet的iOS底层实验室

Lawliet的iOS逆向实验室

Lawliet的刷题小本本

Lawliet的Flutter实验室

Key 的作用

我们创建这样一个示例,看看现象:

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  //key的作用就非常大了!!
  List<Widget> items = [
    ColorItem('第1个'),
    ColorItem('第2个'),
    ColorItem('第3个'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Key的作用'),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: items,
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          setState(() {
            items.removeAt(0);
          });
        },
      ),
    );
  }
}

class ColorItem extends StatefulWidget {
  final String title;

  ColorItem(this.title, {Key? key}) : super(key: key);

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

class _ColorItemState extends State<ColorItem> {
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Text(widget.title),
      color: color,
    );
  }
}
复制代码

23-01.gif

运行发现了上面的现象,作为有 iOS 开发经验的我们,很容易联想到这应该是和复用有关的现象。

复用问题

这里我们需要理解, ColorItem 作为一个 StatefulWidget 它是由两部分组成的: WidgetState 。这里就是 Widget 被移除后, State 还在内存中。所以出现了复用异常的问题。

那么将数据放在 Widget 中按照这里分析的理论来说应该就没有这个现象了吧?我们试试:

class ColorItem extends StatefulWidget {
  final String title;
  // 移到 Widget 中
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);

  ColorItem(this.title, {Key? key}) : super(key: key);

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

class _ColorItemState extends State<ColorItem> {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Text(widget.title),
      color: widget.color,
    );
  }
}
复制代码

23-02.gif

验证成功!那么具体原因是什么呢?

增量渲染与 canUpdate

我们来到 Widget 的实现中, canUpdate 方法决定了一个 WidgetElement 是否会被更新,而 Element 的更新,又直接关系到了增量渲染

同时满足两个条件:

  • 新旧 Widget 的类型相同
  • 新旧 WidgetKey 相同
    • 如果 Key 是空的,只通过类型判断,就算他们的子 Widget 完全不同

这里就很好的解释了一开始发生的异常现象。

/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
}
复制代码

图解

WidgetElement 是一一对应的,而 State 是在 Element 中的。

23-04.png

  • 移除一个 Widget 开始检查
  • 第一个 Element 检查到第二个 Widget
    • 调用 canUpdate
      • 类型相同
      • Key 为空
      • return ture
    • 于是就用 第一个Element 更新 第二个Widget
  • 依次类推,就发生了前面的异常现象

23-05.png

既然这里提到了 Key ,那么加上 Key 是否就可以上面这个问题呢?

Key 的使用

这里为每个 ColorItem 加上 Key , 并将 Color 属性放回 State 中:

...

class _HomePageState extends State<HomePage> {
  //key的作用就非常大了!!
  List<Widget> items = [
    ColorItem(
      '第1个',
      key: ValueKey(1),
    ),
    ColorItem(
      '第2个',
      key: ValueKey(2),
    ),
    ColorItem(
      '第3个',
      key: ValueKey(3),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    ...
  }
}

class ColorItem extends StatefulWidget {
  final String title;
  ...
}

class _ColorItemState extends State<ColorItem> {
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  ...
}

复制代码

23-03.gif

有效!

Key

Key 本身是一个抽象类,有一个工厂方法:

/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=kn0EOS-ZiIc}
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.
@immutable
abstract class Key {
  /// Construct a [ValueKey<String>] with the given [String].
  ///
  /// This is the simplest way to create keys.
  const factory Key(String value) = ValueKey<String>;

  /// Default constructor, used by subclasses.
  ///
  /// Useful so that subclasses can call us, because the [new Key] factory
  /// constructor shadows the implicit constructor.
  @protected
  const Key.empty();
}
复制代码
  • 它有两个子类
    • LocalKey
      • 同一个 父Element 内唯一的
    • GlobalKey
      • 整个 App 内唯一

刚才用的 ValueKeyLocalKey 的子类。

LocalKey

区别哪个 Element 要保留,哪个 Element 要删除。

/// A key that is not a [GlobalKey].
///
/// Keys must be unique amongst the [Element]s with the same parent. By
/// contrast, [GlobalKey]s must be unique across the entire app.
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.
abstract class LocalKey extends Key {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const LocalKey() : super.empty();
}
复制代码
  • ValueKey
    • 以值作为参数(数字、字符串等)
  • ObjectKey
    • 以对象作为参数
  • UniqueKey
    • 创建唯一标识

GlobalKey

在整个 App唯一的KeyGlobalKey 可以唯一标识一个元素, 比如访问一个 BuildContext 或者一个 Widget。对于 StatefulWidget 而言, GlobalKey 也可以访问 State

当有 GlobalKeyWidget 被移动到 Widget树 中新的位置的话,会重新渲染他们的子树。

为了渲染子树,一个 Widget 必须在同一个动画帧内完成在树中,从旧位置移动到新位置。

重新渲染一个使用 GlobalKeyElement 是很消耗性能的,因为会触发调用所有相关的 Statedeactivate 方法,然后让所有依赖 InheritedWidgetWidget 重建。

所以如果你不需要达到上面的效果,那么建议使用其他的 Key

注意点

  • 两个在同一个树中的 Widget 不能同时有相同的 GlobalKey 。尝试这样做的话在运行时会触发异常。
  • GlobalKeys 不应该再每次 build 的时候被重新创建。他们应该是长期被一个 State 所持有的。
    • 例如:
      • 每次 build 的时候都创建一个新的 GlobalKey 会丢弃和 旧Key 相关的子树,并为新Key创建一个新树。除了会损害性能,这种操作还有可能会对子树造成未知的影响。
      • 例如子树中的 GestureDetector 将无法继续追踪正在进行中的手势,因为它会在每次 build 的时候被重新创建
  • 比较好的做法是:
    • 让一个 State 持有这个 GlobalKey ,并且在 Build 方法外初始化它
    • 比如在 State.initState

GlobalKey简单示例

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: GlobalKeyDemo(),
    );
  }
}

class GlobalKeyDemo extends StatelessWidget {

  final GlobalKey<_GKeyItemState> _gKey = GlobalKey();

  GlobalKeyDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Key的作用'),
      ),
      body: Center(
        child: GKeyItem(key: _gKey,),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          _gKey.currentState?.setState(() {
            _gKey.currentState?.count += 1;
          });
        },
      ),
    );
  }
}


class GKeyItem extends StatefulWidget {
  const GKeyItem({Key? key}) : super(key: key);

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

class _GKeyItemState extends State<GKeyItem> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('$count'),
    );
  }
}
复制代码
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改