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

921 阅读6分钟

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 它是由两部分组成的: Widget 和 State 。这里就是 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 方法决定了一个 Widget 的 Element 是否会被更新,而 Element 的更新,又直接关系到了增量渲染。

同时满足两个条件:

  • 新旧 Widget 的类型相同
  • 新旧 Widget 的 Key 相同
    • 如果 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;
}

图解

Widget 和 Element 是一一对应的,而 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 内唯一

刚才用的 ValueKey 是 LocalKey 的子类。

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 中 唯一的Key。 GlobalKey 可以唯一标识一个元素, 比如访问一个 BuildContext 或者一个 Widget。对于 StatefulWidget 而言, GlobalKey 也可以访问 State 。

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

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

重新渲染一个使用 GlobalKey 的 Element 是很消耗性能的,因为会触发调用所有相关的 State 的 deactivate 方法,然后让所有依赖 InheritedWidget 的 Widget 重建。

所以如果你不需要达到上面的效果,那么建议使用其他的 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'),
    );
  }
}