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

相关推荐
老刘忙Giser9 分钟前
C# Process.Start多个参数传递及各个参数之间的空格处理
java·前端·c#
阿珊和她的猫35 分钟前
组件之间的双向绑定:v-model
前端·javascript·vue.js·typescript
爱分享的程序员1 小时前
Node.js 实训专栏规划目录
前端·javascript·node.js
阿迪州1 小时前
iframe作为微前端方案的几个问题
前端·面试
我就是避雷针小鬼啊1 小时前
vue2组件库规划
前端
Burt2 小时前
#🎉 unibest 3.0 发布了!看看都更新了啥好用的功能\~
前端·uni-app
星垂野2 小时前
JavaScript 执行栈和执行上下文详解
前端·javascript
别叫我2 小时前
Swift串行上传多个图片
前端
风铃喵游2 小时前
核心骨架: 小程序双线程架构
前端·架构