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 的。

相关推荐
console.log('npc')17 分钟前
第 1 章 项目概述与环境搭建
前端·人工智能·大模型
定栓18 分钟前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js
IT_陈寒26 分钟前
React组件莫名重渲染?你可能忘了这个小细节
前端·人工智能·后端
鶴哥只手遮天38 分钟前
React 19.3.1 源码深度剖析-第 12 章(下)RSC 与 Flight 协议
前端·react.js·开源
deli00738 分钟前
TCP 三次握手到底发几个包?丢包重传动画实测一遍
前端
盟接之桥1 小时前
盟接之桥谈线束数字化-按单入按单出:线束企业库存管理的破局之道
大数据·前端·网络·数据库·人工智能·制造
巴勒个啦1 小时前
Prompt Engineering 实战:5 个让 AI 生成生产级业务代码的工程模式
java·前端
去伪存真1 小时前
AI时代,古法编程的码农路在何方?
前端
计算机魔术师1 小时前
从7月绕过沙盒到9月再曝违规,OpenAI的AI安全防线在漏什么
前端
我的div丢了肿么办1 小时前
element-plus表格使用:filter-method过滤导致出现无数据
前端·javascript·vue.js