Widget位置移动详细

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

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

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

这是 LocalKey 设计的核心应用场景。比如在一个 ColumnRow 或者 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 对应的 ElementState。它不会去别的地方寻找这个 Key。
  2. 处理父节点2 (Container red):

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

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


为什么 GlobalKey 可以做到?

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

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

  1. setState 触发重建时,Flutter 有一个全局的 Map 记录着所有激活的 GlobalKey 和它们对应的 ElementMap<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 通过一个全局注册表 ,实现了 ElementState 的跨父级迁移,从而保留了状态。


总结与类比

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

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

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

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

相关推荐
GISer_Jing28 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪1 小时前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲6 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter7 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry8 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化