widget的同级移动

在同一个父 Widget 下,子 Widget 只是位置发生了变动(重排序)永远应该优先且几乎总是使用 LocalKey

使用 GlobalKey 在这种场景下虽然也能工作,但属于"杀鸡用牛刀",会带来不必要的性能开销和更高的复杂度。

让我们通过一个例子来详细解释。


场景:一个可重排序的列表

假设我们有一个 Column,它的子节点是一个 StatefulWidget 列表,我们可以通过按钮来改变它们的顺序。

dart 复制代码
class SortableList extends StatefulWidget {
  const SortableList({super.key});

  @override
  State<SortableList> createState() => _SortableListState();
}

class _SortableListState extends State<SortableList> {
  // 数据源
  final List<String> _items = ['Item A', 'Item B', 'Item C'];

  void _swapItems() {
    setState(() {
      // 交换 A 和 C
      final temp = _items[0];
      _items[0] = _items[2];
      _items[2] = temp;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('LocalKey vs GlobalKey')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 根据数据源生成 Widget 列表
          for (final item in _items)
            // 这里是关键:我们应该用什么 Key?
            StatefulItemTile(
              key: /* ??? */, // <--- 关键点
              label: item,
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _swapItems,
        child: const Icon(Icons.swap_vert),
      ),
    );
  }
}

// 一个带状态的瓦片,比如它有自己的随机颜色
class StatefulItemTile extends StatefulWidget {
  final String label;
  const StatefulItemTile({super.key, required this.label});

  @override
  State<StatefulItemTile> createState() => _StatefulItemTileState();
}

class _StatefulItemTileState extends State<StatefulItemTile> {
  // 每个瓦片在创建时都有一个随机颜色,这个颜色就是它的 State
  late final Color _color;

  @override
  void initState() {
    super.initState();
    _color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      padding: const EdgeInsets.all(16),
      color: _color,
      child: Text(widget.label, style: const TextStyle(color: Colors.white, fontSize: 20)),
    );
  }
}

现在,我们来分析 key: /* ??? */ 这里用不同 Key 的情况。

方案一:使用 LocalKey (正确且高效的方式)

我们应该使用一个能够唯一标识数据项的 LocalKey,比如 ValueKey

dart 复制代码
key: ValueKey(item), // item 是 'Item A', 'Item B' 等

工作流程:

  1. 初始状态_items['A', 'B', 'C']
    • Widget 树是 [Tile(key: ValueKey('A')), Tile(key: ValueKey('B')), Tile(key: ValueKey('C'))]
    • Column 的 Element 会创建三个子 Element,每个都有自己的随机颜色 State。
  2. 点击按钮后_items 变成 ['C', 'B', 'A']
    • build 方法生成新的 Widget 树:[Tile(key: ValueKey('C')), Tile(key: ValueKey('B')), Tile(key: ValueKey('A'))]
  3. Column Element 的比对过程
    • 它看到新的第一个 Widget 是 Tile(key: ValueKey('C'))
    • 它会在旧的兄弟 Element 中寻找 ValueKey('C')
    • 找到了!是原来在索引 2 的那个 Element。
    • 结果:它将这个 Element(连同它内部的颜色 State)移动到索引 0 的位置。
    • 同理,ValueKey('B') 对应的 Element 保持在原位,ValueKey('A') 对应的 Element 被移动到索引 2。
  4. 结论 :三个 StatefulItemTile 的状态(颜色)都被完美保留了,并且没有创建新的 Element,只是移动了它们。这是最高效的方式。

方案二:使用 GlobalKey (能工作但没必要)

如果我们为每个 item 预先创建一个 GlobalKey 并存储起来。

dart 复制代码
// 在 State 中提前创建
final Map<String, GlobalKey> _itemKeys = {
  'Item A': GlobalKey(),
  'Item B': GlobalKey(),
  'Item C': GlobalKey(),
};

// 在 build 中使用
key: _itemKeys[item]!,

工作流程: 这个流程和 LocalKey 非常相似。当 Widget 重排序时,Column 的 Element 依然会去匹配 Key。因为 GlobalKey 也能在兄弟节点中被找到,所以它也能正确地移动 Element 并保留状态。

那为什么不推荐?

  1. 性能开销GlobalKey 需要在全局的 Map 中注册和查找,这个开销比 LocalKey 在小范围的兄弟节点中查找要大。对于一个长列表,这种开销会累积。
  2. 管理复杂性 :你需要手动创建并管理这些 GlobalKey 的生命周期,比如用一个 Map 存起来。而 LocalKey 是在 build 方法中即时创建的,用完即弃,非常轻量。
  3. 误用风险GlobalKey 的设计初衷是解决跨父级 移动和外部访问这两个特定问题。在同级重排序这种简单场景下使用它,违背了它的设计意图,可能会让其他维护者感到困惑。

什么时候会出现新旧 Key 不同的情况?

这是一个很好的问题,它关联到 Element 是否会被销毁并重建。

一个 Element 在更新时,它会比较新的 Widget 配置和它自己当前引用的旧 Widget 配置。

canUpdate(newWidget, oldWidget) 这个内部函数会检查 newWidget.runtimeType == oldWidget.runtimeType && newWidget.key == oldWidget.key

新旧 Key 不同的情况主要发生在以下场景,并且会导致 Element 重建:

场景 1:无 Key 或 Key 错误,导致位置与身份混淆

这是最常见的情况。回到上面的例子,如果我们不提供 Key

dart 复制代码
key: null, // 或者干脆不写
  1. 初始状态['A', 'B', 'C'] -> [Tile(A), Tile(B), Tile(C)]
  2. 交换后['C', 'B', 'A'] -> [Tile(C), Tile(B), Tile(A)]
  3. 比对过程 (按索引)
    • 索引 0 :新 Widget 是 Tile(C),旧 Element 是 ElementAruntimeType 相同,key 都是 null,也相同。canUpdate 返回 true 于是,Flutter 认为你只是想把 ElementA 的内容更新成 Tile(C) 的样子。ElementA 被保留,但它的 label 属性从 'A' 变成了 'C'。它的颜色 State (A 的颜色) 没变!
    • 索引 1 :新 Widget 是 Tile(B),旧 Element 是 ElementB。同理,ElementB 被更新,label 没变,颜色也没变。
    • 索引 2 :新 Widget 是 Tile(A),旧 Element 是 ElementCElementC 被更新,label 变成 'A',但保留了 C 的颜色。
  4. 结果 :UI 上显示 C, B, A,但是第一个瓦片的颜色是原来 A 的颜色,第三个瓦片的颜色是原来 C 的颜色。状态错乱了!

场景 2:使用了每次都会变的 Key (如 UniqueKey)

UniqueKeyLocalKey 的一种,但它的特点是每次创建都是一个全新的、唯一的对象。

dart 复制代码
key: UniqueKey(),

如果你在 build 方法里这么写,那么每次 setState 之后,build 方法重新执行,会生成一套全新的 UniqueKey

  1. Column 的 Element 在比对时,会发现新的 Widget 列表里的所有 Key,在旧的 Element 列表里一个都找不到(因为 UniqueKey== 比较是基于对象身份的)。
  2. 结果:所有的旧 Element 都会被销毁,并为新的 Widget 创建全新的 Element 和 State。每次重排都会导致所有瓦片的颜色随机变化。

场景 3:数据源的 ID 发生了变化

假设你的数据源是对象,你用对象的 id 作为 ValueKey

dart 复制代码
// class MyItem { final int id; final String name; }
key: ValueKey(item.id),

如果因为某种操作,你替换了数据源中的一个对象,比如把 MyItem(id: 1, ...) 换成了 MyItem(id: 101, ...)。那么在下一次 build 时:

  • Flutter 会看到一个新的 ValueKey(101)
  • 它在旧的 Element 列表中找不到这个 Key。
  • 同时,旧的 ValueKey(1) 在新的 Widget 列表中也消失了。
  • 结果 :持有 ValueKey(1) 的旧 Element 会被销毁,并为 ValueKey(101) 创建新的 Element。这是符合预期的行为,因为从数据的角度看,这确实是一个全新的项。

总结

  • 同父 Widget 下的子 Widget 重排序 :用 LocalKey(通常是 ValueKeyObjectKey),它轻量、高效、符合设计意图。
  • 新旧 Key 不同会导致 Element 重建 ,这在以下情况会发生:
    1. 你没有提供 Key,导致 Flutter 错误地按位置匹配,并用新 Widget 的数据更新了错误的旧 Element(Key 都是 null,所以 Key 相同)。
    2. 你提供的 Key 是不稳定的,比如 UniqueKey(),导致 Flutter 认为所有 Widget 都是全新的。
    3. 你的数据源的唯一标识符本身发生了变化,导致 Key 自然也发生了变化,这是符合逻辑的重建。
相关推荐
荣达5 分钟前
「CoT」巧思还是骗局?
前端·aigc·产品经理
好记性不如18 分钟前
引入了模块但没有使用”,会不会被打包进去
前端
今天也在写bug22 分钟前
webpack中SplitChunks的分割策略
前端·webpack·性能优化·代码分割·splitchunks
EmpressBoost23 分钟前
解决‘vue‘ 不是内部或外部命令,也不是可运行的程序
开发语言·前端·javascript
ᥬ 小月亮23 分钟前
webpack高级配置
运维·前端·webpack
子林super41 分钟前
mongo创建安全扫描账号手册
前端
用户408128120038143 分钟前
登录权限设置
前端
YYYYY778831 小时前
React Scheduler 原理解读
前端·react.js
子林super1 小时前
Doris-ansible自动化部署脚本
前端
子林super1 小时前
Doris-FE节点滚动重启
前端