Flutter Key 详解:底层原理、类型区别与实际使用场景

2 阅读12分钟

Flutter Key 详解:底层原理、类型区别与实际使用场景

一、先理解 Key 到底解决什么问题

一句话概括:

Key 用来告诉 Flutter:新一轮 build 生成的 Widget,应该与之前的哪个 Element 和 State 对应。

Flutter UI 主要涉及三类对象:

  • Widget:不可变的配置描述,每次 build 都可能重新创建。
  • Element:Widget 在组件树中的实例和位置,生命周期通常比 Widget 长。
  • State:StatefulWidget 对应的可变状态,实际绑定在 StatefulElement 上。

例如:

const UserCard(
  userId: 1001,
  name: 'Alice',
);

UserCard Widget 本身只是配置。真正需要保留的输入框状态、动画状态、计数状态等,通常存放在它对应的 State 中。

当父组件重新 build 时,Flutter 会拿“旧 Widget”和“新 Widget”进行比较。判断逻辑可以简化为:

bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType &&
      oldWidget.key == newWidget.key;
}

也就是说:

runtimeType 相同
并且
Key 相同

=> 复用原来的 ElementState

否则:

删除旧 Element
创建新 Element
创建新的 State

Flutter 官方的 Widget.canUpdate 使用的正是 runtimeTypekey。当两个 Widget 都没有 Key 时,只要类型相同,Flutter就可能认为它们可以互相更新。


二、不设置 Key 时,Flutter 如何匹配 Widget

假设有两个相同类型的组件:

Column(
  children: const [
    UserCard(name: 'Alice'),
    UserCard(name: 'Bob'),
  ],
);

重新 build 后顺序变成:

Column(
  children: const [
    UserCard(name: 'Bob'),
    UserCard(name: 'Alice'),
  ],
);

两个位置上的 Widget 都是:

runtimeType == UserCard
key == null

Flutter默认按位置匹配:

旧位置 0 的 UserCard
更新成
新位置 0 的 UserCard

旧位置 1 的 UserCard
更新成
新位置 1 的 UserCard

因此,它不会自动理解 Alice 和 Bob 交换了位置。

它看到的只是:

第一个 UserCard 还是 UserCard
第二个 UserCard 还是 UserCard

如果 UserCard 是 StatefulWidget,它的 State 可能继续留在原来的位置,而不是跟着 Alice 或 Bob 移动。

这就是列表增加、删除、排序后出现状态错位的主要原因。


三、可运行示例:相同 StatefulWidget 交换位置

下面的代码可以直接运行。

1. 错误版本:没有 Key

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: SwapDemoPage(),
    ),
  );
}

class SwapDemoPage extends StatefulWidget {
  const SwapDemoPage({super.key});

  @override
  State<SwapDemoPage> createState() => _SwapDemoPageState();
}

class _SwapDemoPageState extends State<SwapDemoPage> {
  List<String> users = ['Alice', 'Bob'];

  void swapUsers() {
    setState(() {
      users = users.reversed.toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Key Demo'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: swapUsers,
            child: const Text('交换位置'),
          ),
          ...users.map(
            (name) => UserCounterCard(
              name: name,
            ),
          ),
        ],
      ),
    );
  }
}

class UserCounterCard extends StatefulWidget {
  const UserCounterCard({
    super.key,
    required this.name,
  });

  final String name;

  @override
  State<UserCounterCard> createState() => _UserCounterCardState();
}

class _UserCounterCardState extends State<UserCounterCard> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(widget.name),
      subtitle: Text('Count: $count'),
      trailing: IconButton(
        icon: const Icon(Icons.add),
        onPressed: () {
          setState(() {
            count++;
          });
        },
      ),
    );
  }
}

操作步骤

  1. 给 Alice 点击几次加号。
  2. 点击“交换位置”。
  3. Alice 和 Bob 的顺序交换。
  4. 计数可能仍留在原来的位置。

问题原因

Flutter看到的结构是:

位置 0:UserCounterCard
位置 1:UserCounterCard

交换后仍然是:

位置 0:UserCounterCard
位置 1:UserCounterCard

由于类型相同并且都没有 Key,Flutter复用了原位置上的 State。


2. 正确版本:添加 ValueKey

修改列表构建代码:

...users.map(
  (name) => UserCounterCard(
    key: ValueKey(name),
    name: name,
  ),
),

现在 Flutter 可以识别:

ValueKey('Alice')
ValueKey('Bob')

交换位置后,Alice 对应的 State 会跟着 Alice 移动,而不是留在原位置。


四、Flutter Key 的分类关系

Flutter Key 大致分成两类:

Key
├── LocalKey
│   ├── ValueKey
│   │   └── PageStorageKey
│   ├── ObjectKey
│   └── UniqueKey
│
└── GlobalKey
    ├── LabeledGlobalKey
    └── GlobalObjectKey

LocalKey 只要求在同一个父节点的直接子节点中保持唯一;GlobalKey 则必须在整个应用当前组件树中保持唯一。


五、Key

1. Key 是什么

Key 是所有 Flutter Key 的基础抽象类。

abstract class Key

一般不会直接继承或实例化抽象类,但 Flutter 提供了一个字符串构造方式:

const Key('user-1001')

它实际上等价于:

const ValueKey<String>('user-1001')

官方实现是:

const factory Key(String value) = ValueKey<String>;

2. 使用场景

适合简单的字符串标识:

ListTile(
  key: const Key('settings-notification-item'),
  title: const Text('Notifications'),
);

3. 不适合的场景

当标识值不是字符串时,应该明确使用:

ValueKey<int>(user.id)

而不是先转字符串:

Key(user.id.toString())

虽然两种写法都能工作,但 ValueKey<int> 类型信息更加明确。

4. 风险

Key 本身没有特殊性能风险。

真正的问题通常是:

  • Key 不稳定;
  • Key 重复;
  • Key 放错层级;
  • 使用会不断变化的值作为 Key。

六、ValueKey

1. 作用

ValueKey<T> 使用一个值判断两个 Key 是否相同。

const ValueKey<int>(1001)

两个 ValueKey 相等,需要满足:

Key 类型相同
并且
value == other.value

例如:

const ValueKey<int>(1) == const ValueKey<int>(1)

结果为:

true

ValueKey 使用传入值的 ==hashCode 判断身份。

2. 最适合的场景

  • 数据库 ID;
  • 用户 ID;
  • 消息 ID;
  • 商品 ID;
  • 稳定且唯一的业务字段;
  • AnimatedSwitcher 的内容变化;
  • ReorderableListView;
  • 可以增加、删除或排序的列表。

3. 推荐写法

ListView.builder(
  itemCount: users.length,
  itemBuilder: (context, index) {
    final user = users[index];

    return UserCard(
      key: ValueKey(user.id),
      user: user,
    );
  },
);

4. 不推荐使用索引作为 Key

错误:

UserCard(
  key: ValueKey(index),
  user: user,
);

假设原列表:

index 0 -> Alice
index 1 -> Bob
index 2 -> Carol

删除 Alice 后:

index 0 -> Bob
index 1 -> Carol

Bob 的 Key 从:

ValueKey(1)

变成:

ValueKey(0)

这不是稳定身份。

正确:

UserCard(
  key: ValueKey(user.id),
  user: user,
);

5. 使用可变值的风险

错误:

UserCard(
  key: ValueKey(user.name),
  user: user,
);

如果用户修改名字,Key 也会变化:

Alice -> Alicia

Flutter会认为原来的组件被删除,并创建一个新组件,导致:

  • State 重置;
  • 输入内容丢失;
  • 动画重新开始;
  • Controller 重新创建;
  • 焦点丢失。

应该使用稳定 ID:

ValueKey(user.id)

6. 性能和内存风险

ValueKey 本身很轻量。

主要风险不是内存泄漏,而是业务值不稳定导致错误地销毁和重建 State。


七、ObjectKey

1. 作用

ObjectKey 使用对象实例本身作为身份。

ObjectKey(user)

它比较的不是普通的 ==,而是:

identical(oldObject, newObject)

也就是说,必须是内存中的同一个对象实例。

2. 示例

final user = User(
  id: 1001,
  name: 'Alice',
);

UserCard(
  key: ObjectKey(user),
  user: user,
);

只要每次 build 使用的仍然是同一个 user 对象,Key 就相同。

3. ObjectKey 与 ValueKey 的区别

假设有两个内容相同但实例不同的对象:

final user1 = User(id: 1001, name: 'Alice');
final user2 = User(id: 1001, name: 'Alice');

使用:

ObjectKey(user1)
ObjectKey(user2)

结果不同,因为:

identical(user1, user2) == false

使用:

ValueKey(user1.id)
ValueKey(user2.id)

结果相同,因为:

user1.id == user2.id

4. 适合场景

适合:

  • 对象实例本身代表身份;
  • 列表长期复用同一批 Model 实例;
  • 数据对象没有稳定 ID;
  • 不同对象即使内容相同,也必须被视为不同项目。

5. 不适合场景

很多 API 和状态管理框架会不断创建新的 Model:

User.fromJson(json)

即使用户 ID 没变,每次刷新后也会得到新的对象实例。

此时使用:

ObjectKey(user)

Key 每次都会变化。

这种情况应该使用:

ValueKey(user.id)

6. 风险

主要风险是误以为它根据对象内容比较。

实际上:

旧对象和新对象内容一样
但不是同一个实例

=> ObjectKey 不相同
=> State 可能被重建

八、UniqueKey

1. 作用

每次执行:

UniqueKey()

都会创建一个只与自身相等的 Key。

final key1 = UniqueKey();
final key2 = UniqueKey();

print(key1 == key2); // false

UniqueKey 不能使用 const,因为如果是 const,Dart可能复用相同实例,这会违背“每个 Key 都唯一”的设计。

2. 适合场景

适合没有业务 ID,但需要给当前对象生成一次性唯一身份的场景。

例如:

class TodoItem {
  TodoItem({
    required this.title,
  }) : key = UniqueKey();

  final String title;
  final Key key;
}

使用:

TodoRow(
  key: todo.key,
  todo: todo,
);

注意:UniqueKey 存在 Model 中,因此不会在每次 build 时重新生成。

3. 强制重置子组件

class ResetDemo extends StatefulWidget {
  const ResetDemo({super.key});

  @override
  State<ResetDemo> createState() => _ResetDemoState();
}

class _ResetDemoState extends State<ResetDemo> {
  Key childKey = UniqueKey();

  void resetChild() {
    setState(() {
      childKey = UniqueKey();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CounterPanel(
          key: childKey,
        ),
        ElevatedButton(
          onPressed: resetChild,
          child: const Text('重置子组件'),
        ),
      ],
    );
  }
}

每次点击按钮才创建新的 UniqueKey,因此 Flutter 会删除旧 State 并创建新 State。

4. 严重错误:在 build 中直接创建

错误:

@override
Widget build(BuildContext context) {
  return UserCard(
    key: UniqueKey(),
    user: user,
  );
}

每次父组件 build:

 UniqueKey !=  UniqueKey

Flutter会认为这是一个全新的 Widget。

可能导致:

  • State 每次被重置;
  • 输入框内容丢失;
  • 列表闪烁;
  • 动画重复开始;
  • 焦点丢失;
  • 性能下降。

5. 正确写法

在 State 中只创建一次:

late final Key userCardKey;

@override
void initState() {
  super.initState();
  userCardKey = UniqueKey();
}

然后:

UserCard(
  key: userCardKey,
  user: user,
);

九、GlobalKey

1. 作用

GlobalKey 是整个应用当前组件树中全局唯一的 Key。

它主要提供三个特殊能力:

  1. 获取 Widget 对应的 State;
  2. 获取 BuildContext;
  3. 在组件树中移动子树时保留其 Element 和 State。
final key = GlobalKey<MyWidgetState>();

key.currentState;
key.currentContext;
key.currentWidget;

Flutter 官方说明,GlobalKey 能全局识别 Element,并可通过 currentStatecurrentContextcurrentWidget 访问相关对象。


2. 表单验证场景

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();

  void submit() {
    final isValid = formKey.currentState?.validate() ?? false;

    if (!isValid) {
      return;
    }

    formKey.currentState?.save();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formKey,
      child: Column(
        children: [
          TextFormField(
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return 'Please enter your email.';
              }

              return null;
            },
          ),
          ElevatedButton(
            onPressed: submit,
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

Flutter 官方同时支持两种获取 FormState 的方式:

Form.of(context)

或者:

GlobalKey<FormState>.currentState

如果按钮位于 Form 的子树中,通常可以考虑:

Form.of(context)

这样可以避免额外使用 GlobalKey。


3. 父组件调用子组件方法

class ParentPage extends StatefulWidget {
  const ParentPage({super.key});

  @override
  State<ParentPage> createState() => _ParentPageState();
}

class _ParentPageState extends State<ParentPage> {
  final GlobalKey<CounterBoxState> counterKey =
      GlobalKey<CounterBoxState>();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CounterBox(
          key: counterKey,
        ),
        ElevatedButton(
          onPressed: () {
            counterKey.currentState?.increment();
          },
          child: const Text('父组件增加计数'),
        ),
      ],
    );
  }
}

class CounterBox extends StatefulWidget {
  const CounterBox({
    super.key,
  });

  @override
  State<CounterBox> createState() => CounterBoxState();
}

class CounterBoxState extends State<CounterBox> {
  int count = 0;

  void increment() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

4. 是否推荐父组件直接调用子组件方法

技术上可以,但不应该成为默认的组件通信方式。

普通业务状态更适合使用:

  • 回调;
  • ValueNotifier;
  • ChangeNotifier;
  • Bloc;
  • Provider;
  • Riverpod;
    -共享 ViewModel。

GlobalKey 更适合:

  • FormState;
  • ScaffoldMessenger 等框架状态;
  • 特殊组件控制;
  • 获取组件尺寸和位置;
  • 必须直接操作某个 State 的少量场景。

5. 获取组件位置和大小

final GlobalKey boxKey = GlobalKey();

void printSizeAndPosition() {
  final context = boxKey.currentContext;

  if (context == null) {
    return;
  }

  final renderObject = context.findRenderObject();

  if (renderObject is! RenderBox) {
    return;
  }

  final size = renderObject.size;
  final position = renderObject.localToGlobal(Offset.zero);

  debugPrint('size: $size');
  debugPrint('position: $position');
}

组件:

Container(
  key: boxKey,
  width: 200,
  height: 100,
  child: const Text('Target'),
);

应当在布局完成后读取:

WidgetsBinding.instance.addPostFrameCallback((_) {
  printSizeAndPosition();
});

因为在 build 期间,RenderObject 可能还没有完成当前帧布局。


6. 为什么不要大量使用 GlobalKey

GlobalKey 的匹配范围是整个应用组件树,而不是同一个父节点。

当带 GlobalKey 的组件从一个位置移动到另一个位置时,Flutter可以把整个子树重新挂载到新位置并保留 State。但是这个过程相对昂贵,会触发相关 State 的 deactivate,并可能使依赖 InheritedWidget 的子组件重新 build。

因此,当只需要列表身份时,不要使用:

GlobalKey()

应该使用:

ValueKey(item.id)

7. 严重错误:每次 build 创建 GlobalKey

错误:

@override
Widget build(BuildContext context) {
  final formKey = GlobalKey<FormState>();

  return Form(
    key: formKey,
    child: const Placeholder(),
  );
}

每次 build 都会得到新的 GlobalKey。

结果可能包括:

  • 原子树 State 被丢弃;
  • 表单状态重置;
  • 手势中断;
  • 组件重新初始化;
  • 不必要的性能损耗。

Flutter 官方明确建议 GlobalKey 由 State 长期持有,并在 build 外创建,例如字段初始化或 initState

正确:

class _PageState extends State<Page> {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formKey,
      child: const Placeholder(),
    );
  }
}

8. 重复使用同一个 GlobalKey

错误:

final key = GlobalKey();

Column(
  children: [
    Container(key: key),
    Container(key: key),
  ],
);

两个组件同时存在于组件树中,但使用了同一个 GlobalKey。

Flutter会在运行时抛出断言错误,因为同一个 GlobalKey 不能同时绑定两个 Element。

跨页面共享 GlobalKey 同样需要注意。

例如 Page A 尚未完全移除,Page B 已经开始构建,在页面切换动画期间两个页面可能同时存在于组件树中。这时如果两个页面使用同一个 GlobalKey,也可能发生冲突。


9. GlobalKey 是否会导致内存泄漏

GlobalKey 本身不等于内存泄漏。

更常见的问题是:

  • 把 GlobalKey 放在长期存在的静态变量或单例中;
  • 长期保存 currentContextcurrentState
  • State 中的 Controller、Stream、AnimationController 没有 dispose;
  • GlobalKey 的生命周期远长于页面实际生命周期。

推荐:

class _PageState extends State<Page> {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
}

不推荐无必要地定义成:

static final GlobalKey<FormState> formKey =
    GlobalKey<FormState>();

Key 应该由真正拥有它的页面或组件负责管理。


十、GlobalObjectKey

1. 作用

GlobalObjectKey 是全局版的 ObjectKey。

它使用对象实例作为全局身份:

GlobalObjectKey(user)

任何使用同一个对象实例创建的 GlobalObjectKey 都会匹配。

它比较的是:

identical(oldValue, newValue)

而不是普通的 ==

2. 示例

class UserEditor extends StatefulWidget {
  const UserEditor({
    super.key,
    required this.user,
  });

  final User user;

  @override
  State<UserEditor> createState() => UserEditorState();
}

class UserEditorState extends State<UserEditor> {
  void save() {
    debugPrint('Save ${widget.user.name}');
  }

  @override
  Widget build(BuildContext context) {
    return Text(widget.user.name);
  }
}

使用:

final User alice = User(
  id: 1001,
  name: 'Alice',
);

UserEditor(
  key: GlobalObjectKey<UserEditorState>(alice),
  user: alice,
);

之后只要仍然使用同一个 alice 实例,就可以通过同等的 GlobalObjectKey 找到对应 State。

3. 更安全的私有 Key 类型

不同模块可能误用同一个对象创建 GlobalObjectKey,从而发生全局冲突。

官方建议可以创建私有子类:

class _UserEditorKey
    extends GlobalObjectKey<UserEditorState> {
  const _UserEditorKey(super.value);
}

使用:

UserEditor(
  key: _UserEditorKey(alice),
  user: alice,
);

因为 Key 的 runtimeType 也是其身份的一部分,私有子类可以降低不同模块之间发生冲突的风险。

4. 不适合场景

如果只是普通列表排序,不需要全局访问 State:

ValueKey(user.id)

通常比:

GlobalObjectKey(user)

更简单、更安全。


十一、PageStorageKey

1. 作用

PageStorageKey 用于在最近的 PageStorage 中保存和恢复部分页面状态。

最常见的是:

  • 列表滚动位置;
  • Tab 中不同列表的滚动位置;
  • 页面切换后恢复 ScrollView 位置;
  • ExpansionTile 展开状态等支持 PageStorage 的组件状态。

它本质上继承自:

ValueKey<T>

Flutter 官方说明,PageStorageKey 的值及其祖先 PageStorageKey 链必须在最近的 PageStorage 中保持唯一,并且 Key 的值不能在每次重建时改变身份。

2. 多个 Tab 的列表

class ListTab extends StatelessWidget {
  const ListTab({
    super.key,
    required this.tabId,
  });

  final String tabId;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      key: PageStorageKey<String>('list-$tabId'),
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('$tabId item $index'),
        );
      },
    );
  }
}

使用:

TabBarView(
  children: const [
    ListTab(tabId: 'likes'),
    ListTab(tabId: 'visitors'),
    ListTab(tabId: 'messages'),
  ],
);

每个列表都有不同的 PageStorageKey:

list-likes
list-visitors
list-messages

切换 Tab 后,Flutter可以区分并恢复不同列表的滚动位置。

ScrollView 默认会尝试通过 PageStorage 保存滚动位置。当同一个页面存在多个 ScrollView 时,官方建议使用 PageStorageKey 区分它们。

3. 错误写法

ListView(
  key: PageStorageKey(DateTime.now()),
  children: children,
);

每次 build 的值都不同,旧状态无法被找到。

正确:

const PageStorageKey<String>('message-list')

4. PageStorageKey 与持久化的区别

PageStorageKey 不是:

  • 数据库;
  • SharedPreferences;
  • 用户账号云同步;
  • 完整的进程重启状态恢复方案。

它主要解决当前 Widget/PageStorage 生命周期内的页面状态恢复。

如果需要应用被系统杀死后恢复状态,应进一步研究 Flutter 的 State Restoration 和 restorationId


十二、LabeledGlobalKey

1. 作用

LabeledGlobalKey 是带调试标签的 GlobalKey。

final key = GlobalKey<FormState>(
  debugLabel: 'loginForm',
);

这里创建出来的实际实现就是 LabeledGlobalKey。

标签只用于:

  • 调试;
  • 日志;
  • DevTools;
  • 错误信息;
  • 区分多个 GlobalKey。

标签不会参与 Key 身份比较。即使两个标签相同,也仍然是两个不同的 GlobalKey。

2. 示例

final GlobalKey<FormState> loginFormKey =
    GlobalKey<FormState>(
  debugLabel: 'login-form',
);

final GlobalKey<FormState> registerFormKey =
    GlobalKey<FormState>(
  debugLabel: 'register-form',
);

3. 相同标签不代表相同 Key

final key1 = GlobalKey(debugLabel: 'form');
final key2 = GlobalKey(debugLabel: 'form');

print(key1 == key2); // false

标签只是为了方便开发人员看懂:

这个 GlobalKey 属于哪个页面或组件

4. 使用建议

一般不需要直接写:

LabeledGlobalKey<FormState>('login-form')

更常见的写法是:

GlobalKey<FormState>(
  debugLabel: 'login-form',
)

十三、AnimatedSwitcher 为什么需要 Key

错误示例

AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  child: Text(
    '$count',
  ),
);

虽然文本内容从:

1

变成:

2

但是旧组件和新组件都是:

runtimeType == Text
key == null

Flutter认为它仍然是同一个 Widget 的参数更新,因此可能不会执行预期的切换动画。

正确写法

AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  child: Text(
    '$count',
    key: ValueKey<int>(count),
  ),
);

当 count 改变:

ValueKey(1) != ValueKey(2)

AnimatedSwitcher 会把它识别为新的 child,并执行进入和退出动画。Flutter 官方也建议在需要区分 AnimatedSwitcher 子组件时使用 Key,通常是基于数据的 ValueKey。


十四、ReorderableListView 中的 Key

ReorderableListView 要求所有列表项都有 Key。

正确示例

class Todo {
  const Todo({
    required this.id,
    required this.title,
  });

  final int id;
  final String title;
}
ReorderableListView(
  children: todos.map((todo) {
    return ListTile(
      key: ValueKey(todo.id),
      title: Text(todo.title),
    );
  }).toList(),
  onReorder: (oldIndex, newIndex) {
    setState(() {
      if (newIndex > oldIndex) {
        newIndex--;
      }

      final item = todos.removeAt(oldIndex);
      todos.insert(newIndex, item);
    });
  },
);

错误示例

key: ValueKey(index)

拖动后,index 代表位置,而不是数据身份。

正确:

key: ValueKey(todo.id)

十五、Key 应该添加在哪一层

一个重要原则:

Key 应该放在发生增加、删除或顺序变化的那组直接子节点上。

错误示例

Column(
  children: users.map((user) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: UserCard(
        key: ValueKey(user.id),
        user: user,
      ),
    );
  }).toList(),
);

Column 看来,它的直接子节点是:

Padding
Padding
Padding

这些 Padding 都没有 Key。

Key 放在更里面的 UserCard 上,无法帮助 Column 正确识别哪个直接子节点发生了移动。

正确写法

Column(
  children: users.map((user) {
    return Padding(
      key: ValueKey(user.id),
      padding: const EdgeInsets.all(8),
      child: UserCard(
        user: user,
      ),
    );
  }).toList(),
);

也可以使用:

KeyedSubtree(
  key: ValueKey(user.id),
  child: Padding(
    padding: const EdgeInsets.all(8),
    child: UserCard(
      user: user,
    ),
  ),
);

判断方法

问自己:

哪一组兄弟节点会发生顺序变化?

Key 就应该放在这组兄弟节点的最外层。


十六、ValueKey、ObjectKey 和 UniqueKey 如何选择

有稳定业务 ID

使用:

ValueKey(item.id)

这是绝大多数列表场景的首选。

没有 ID,但对象实例长期稳定

使用:

ObjectKey(item)

前提是每次 build 仍然使用同一个对象实例。

没有 ID,也没有合适的对象身份

创建一次:

final key = UniqueKey();

然后长期保存这个 Key。

想强制重置 State

在需要重置的时刻创建新的:

setState(() {
  childKey = UniqueKey();
});

不要在每次 build 中创建。


十七、LocalKey 和 GlobalKey 的本质区别

对比项LocalKeyGlobalKey
唯一范围同一个父节点的直接子节点整个应用当前组件树
常见类型ValueKey、ObjectKey、UniqueKeyGlobalKey、GlobalObjectKey
主要目的列表匹配、排序、删除、动画获取 State/Context、跨位置移动子树
性能成本通常较低相对较高
是否可访问 State不可以可以
是否可访问 Context不可以可以
是否允许跨父节点保留子树不可以可以
是否适合普通列表通常不适合

十八、const Widget 与 Key 的关系

1. const 不能代替 Key

const UserCard(
  name: 'Alice',
)

const 的主要作用是让不可变 Widget 可以在编译期创建和复用,减少重复对象创建。

但它没有表达:

Alice 是哪个业务对象

列表顺序变化时,仍然需要:

const UserCard(
  key: ValueKey('alice'),
  name: 'Alice',
)

2. const ValueKey

ValueKey 支持 const:

const ValueKey<String>('alice')

只要 value 相同,它们就相等。

3. UniqueKey 不支持 const

错误:

const UniqueKey()

UniqueKey 必须保证每个实例都不同,因此没有 const 构造。

4. const 不保证 State 永远保留

是否保留 State,最终仍取决于:

Widget 在树中的位置
runtimeType
Key
组件树更新过程

不能简单理解为:

用了 const,State 就不会重建

十九、常见错误总结

错误一:列表使用 index 作为 Key

ValueKey(index)

问题表现

  • 删除数据后状态错位;
  • 输入内容跑到其他列表项;
  • Switch 状态与数据不一致。

正确写法

ValueKey(item.id)

错误二:build 中创建 UniqueKey

Widget build(BuildContext context) {
  return Editor(
    key: UniqueKey(),
  );
}

问题表现

  • 每次 build 都重置;
  • 输入内容丢失;
  • 动画反复启动。

正确写法

final Key editorKey = UniqueKey();

错误三:build 中创建 GlobalKey

final key = GlobalKey<FormState>();

问题表现

  • FormState 重置;
  • 子树反复创建;
  • currentState 不稳定。

正确写法

class _PageState extends State<Page> {
  final key = GlobalKey<FormState>();
}

错误四:同一个 GlobalKey 同时使用两次

Column(
  children: [
    Form(key: formKey, child: child1),
    Form(key: formKey, child: child2),
  ],
);

问题表现

Flutter抛出重复 GlobalKey 的运行时断言。

正确写法

final firstFormKey = GlobalKey<FormState>();
final secondFormKey = GlobalKey<FormState>();

错误五:Key 放在太内层

Padding(
  child: UserCard(
    key: ValueKey(user.id),
  ),
)

问题表现

外层列表仍然按无 Key 的 Padding 匹配,State 可能无法跟随数据移动。

正确写法

Padding(
  key: ValueKey(user.id),
  child: UserCard(),
)

错误六:使用可变化字段作为 Key

ValueKey(user.name)

问题表现

用户修改名字后,Flutter将组件识别为新组件,旧 State 被销毁。

正确写法

ValueKey(user.id)

二十、Key 类型完整对比表

Key 类型唯一性判断方式生命周期典型场景优点缺点与注意事项
Key('value')字符串值的 ==跟随 Widget 配置简单字符串标识写法简洁实际是 ValueKey<String>
ValueKey<T>value 的 ==hashCode由传入值决定用户 ID、消息 ID、列表排序、动画最常用、语义清楚value 必须稳定且唯一
ObjectKey对象实例的 identical跟随对象实例对象实例本身代表身份不依赖对象字段API 重建对象后 Key 会变化
UniqueKey只与自身相等由 Key 实例决定无业务 ID、强制重置 State保证唯一不能在每次 build 中创建
GlobalKeyKey 实例全局唯一应由 State 长期持有FormState、获取 State/Context、测量组件功能强大匹配成本高,不能重复,不能频繁创建
GlobalObjectKey对象实例的 identical,全局范围跟随对象和组件树根据对象全局定位 State可重新创建同等 Key 找到同一对象容易出现全局冲突,通常应使用私有子类
PageStorageKeyvalue 的 ==,结合祖先 Key 链最近的 PageStorage 生命周期保存列表滚动位置、Tab 页面状态简单解决页面状态恢复不是磁盘持久化,value 必须稳定
LabeledGlobalKeyGlobalKey 实例身份应长期持有带调试名称的 GlobalKey日志和调试更清晰debugLabel 不参与身份判断

二十一、实际项目中的选择原则

大多数业务可以按照以下顺序选择:

1. 不涉及列表增加、删除、排序或状态保留
   => 通常不需要 Key

2. 有稳定业务 ID
   => ValueKey(id)

3. 对象实例本身就是身份
   => ObjectKey(object)

4. 没有业务 ID,但需要一次性唯一身份
   => 创建一次 UniqueKey 并保存

5. 需要恢复滚动位置
   => PageStorageKey

6. 需要访问 FormStateStateContextRenderObject
   => GlobalKey

7. 需要通过对象全局定位组件
   => GlobalObjectKey

8. 只是为了让 GlobalKey 更容易调试
   => GlobalKey(debugLabel: 'name')

最重要的原则是:

能使用 ValueKey,就不要使用 GlobalKey。

以及:

Key 必须代表数据或组件的稳定身份,而不是它当前所在的位置。