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 自然也发生了变化,这是符合逻辑的重建。
相关推荐
粥里有勺糖1 分钟前
视野修炼第124期 | 终端艺术字
前端·javascript·github
zhangxingchao16 分钟前
Flutter常见Widget的使用
前端
aiweker24 分钟前
python web开发-Flask数据库集成
前端·python·flask
暴怒的代码30 分钟前
解决Vue2官网Webpack源码泄露漏洞
前端·webpack·node.js
老刘忙Giser43 分钟前
C# Process.Start多个参数传递及各个参数之间的空格处理
java·前端·c#
阿珊和她的猫1 小时前
组件之间的双向绑定:v-model
前端·javascript·vue.js·typescript
爱分享的程序员2 小时前
Node.js 实训专栏规划目录
前端·javascript·node.js
阿迪州2 小时前
iframe作为微前端方案的几个问题
前端·面试
我就是避雷针小鬼啊2 小时前
vue2组件库规划
前端
Burt2 小时前
#🎉 unibest 3.0 发布了!看看都更新了啥好用的功能\~
前端·uni-app