Widget位置移动详细

"位置变动"有两种完全不同的情况:

  1. 在同一父节点下,兄弟节点之间的位置变动(重排序)。
  2. 跨越不同父节点的位置变动(Reparenting / 移植)。

1. 兄弟节点之间的位置变动 (LocalKey 的主场)

这是 LocalKey 设计的核心应用场景。比如在一个 Column、Row 或者 ListView 中。

例子:交换两个带 LocalKey 的 Widget

dart 复制代码
// 初始状态
Column(
  children: [
    StatefulWidgetA(key: ValueKey('A')),
    StatefulWidgetB(key: ValueKey('B')),
  ],
)

当 setState 之后,我们交换它们的位置:

dart 复制代码
// 更新后的状态
Column(
  children: [
    StatefulWidgetB(key: ValueKey('B')),
    StatefulWidgetA(key: ValueKey('A')),
  ],
)

Flutter 的处理流程 (使用 LocalKey):

  1. Flutter 看到新的子 Widget 列表是 [WidgetB, WidgetA]。
  2. 它看到旧的子 Element 列表是 [ElementA, ElementB]。
  3. 对于新的第一个 Widget WidgetB(key: ValueKey('B')),它在旧的兄弟节点 中查找 ValueKey('B')。
  4. 找到了!是旧的 ElementB。Flutter 会复用 ElementB 及其 State,并把它更新到新的位置(索引 0)。
  5. 同理,WidgetA 也会找到并复用 ElementA。

结论: 在这种情况下,LocalKey 完美地保留了 State 。这是它的核心功能。如果你发现没有保留,通常是因为 Key 设置错误(比如用了 UniqueKey(),它每次 build 都会生成新的 Key),或者 Widget 类型发生了变化。


2. 跨父节点的位置变动 (GlobalKey 的主场)

这是你问题中描述的"位置变动"最可能指向的场景,也是 LocalKey 无能为力的场景。

例子:将 Widget 从一个 Container 移动到另一个 Container

dart 复制代码
// 假设 isFirstParent 为 true
Column(
  children: [
    Container( // 父节点1
      color: Colors.blue,
      child: isFirstParent ? StatefulWidgetA(key: ValueKey('A')) : SizedBox(),
    ),
    Container( // 父节点2
      color: Colors.red,
      child: !isFirstParent ? StatefulWidgetA(key: ValueKey('A')) : SizedBox(),
    ),
  ],
)

当 setState 之后,我们把 isFirstParent 设为 false。

Flutter 的处理流程 (使用 LocalKey):

  1. 处理父节点1 (Container blue):

    • 旧的子 Widget 是 StatefulWidgetA(key: ValueKey('A'))。
    • 新的子 Widget 是 SizedBox()。
    • 由于类型和 Key 都不匹配,Flutter 会销毁 StatefulWidgetA 对应的 Element 和 State。它不会去别的地方寻找这个 Key。
  2. 处理父节点2 (Container red):

    • 旧的子 Widget 是 SizedBox()。
    • 新的子 Widget 是 StatefulWidgetA(key: ValueKey('A'))。
    • 这是一个全新的创建过程。Flutter 会为 StatefulWidgetA 创建一个新的 Element 和一个新的 State。

结论: 在这种情况下,StatefulWidgetA 的状态丢失了 。因为 LocalKey 的查找范围仅限于其原来的兄弟节点 。当 StatefulWidgetA 从 Container blue 中被移除时,它的 Element 就被释放了。Container red 在构建自己的子节点时,无法"看到"或"访问"那个刚刚被销毁的 Element。


为什么 GlobalKey 可以做到?

现在我们把 ValueKey('A') 换成 GlobalKey()。

Flutter 的处理流程 (使用 GlobalKey):

  1. 当 setState 触发重建时,Flutter 有一个全局的 Map 记录着所有激活的 GlobalKey 和它们对应的 Element。Map<GlobalKey, Element>。

  2. 处理父节点1 (Container blue):

    • Flutter 看到 StatefulWidgetA 将要被移除。但它注意到这个 Widget 有一个 GlobalKey。
    • 它不会立即销毁 ElementA。而是暂时将它从树中分离出来,但保留在内存中,并标记为"待认领"。
  3. 处理父节点2 (Container red):

    • Flutter 看到这里需要构建一个新的 StatefulWidgetA,并且它带有一个 GlobalKey。
    • 它会去全局的 Map 中检查:"嘿,这个 GlobalKey 我是不是在哪里见过?"
    • 它发现这个 GlobalKey 正指向那个刚刚从 Container blue 中分离出来的、"待认领"的 ElementA。
    • Bingo! Flutter 不会创建新的 Element,而是直接将这个旧的 ElementA(连同它宝贵的 State)"移植" 过来,挂载到 Container red 下面。

结论: GlobalKey 通过一个全局注册表 ,实现了 Element 和 State 的跨父级迁移,从而保留了状态。


总结与类比

  • LocalKey 就像一个班级里的学生学号。

    • 在班级内部(兄弟节点),老师可以通过学号准确找到张三李四,即使他们换了座位(重排序),也能认出他们。
    • 但如果张三转学到了另一个班级(跨父节点),新班级的老师是不知道他原来的学号的,会给他一个新的学号(创建新 State)。
  • GlobalKey 就像一个全国唯一的身份证号。

    • 无论张三转到哪个学校、哪个班级(在 Widget 树中如何移动),只要出示身份证,系统就能识别出"哦,还是原来那个张三",他所有的个人档案(State)都会被完整地迁移过来。

LocalKey 的 Widget 在"跨父级"位置变动后,State 没有保留,是因为 LocalKey 的作用域是局部的,它无法实现 Element 的"跨级传送"。 而在"同级"位置变动(重排序)时,LocalKey 是会保留 State 的。

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大5 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师5 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学5 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端