flutter:widget的key

为什么需要 key

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({super.key});
}
@immutable
abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({this.key});
  final Key? key;
}
  • 在 Flutter 里,任何 widget 都可以有一个 key,它是 Flutter 框架 高效管理 widget 树 的核心机制之一。

  • 在 Flutter 里,界面是通过不断 重新构建 widget 树 实现刷新的。但是 widget 是 immutable(不可变) 的,一旦变化就会创建新的对象。

  • 因此,框架需要一种方法判断:新旧 widget 是否对应

  • 在大部分情况下,其实不需要判断新旧 widget 是否对应,因为大部分是静态 UI不需要 key

  • 只有在widget的动态 UI时,这个时候必须用 key

    • 列表/ListView/GridView:items 增删/重排时,如果没 key,Flutter 只按位置匹配,内容和输入状态可能错乱。
    • 动画/切换时:比如 PageViewTabBarView,不同页面的子 widget 类型一样,但其实身份不同,需要 key 区分。
    • 复用相同类型 widget 但身份不同:例如两个 TextField 都是 TextField 类型,但一个是“用户名”,另一个是“密码”,如果它们在树里的相对顺序发生变化,就必须用 key 区分,不然输入内容会串。

key 的作用唯一标识一个 widget

flutter 的匹配复用规则

匹配和复用的真正含义

Flutter 的三层概念

理解复用,先要知道 Flutter 的三层概念:

  • Widget
    • 轻量级、不可变(immutable),只是一个配置。
    • 每次 build() 时,都会重新创建新的 widget 对象。
  • Element
    • Widget 在树中的“位置”和“桥梁”,管理生命周期。
    • 持有 widget + state。
  • RenderObject:负责真正的绘制和布局,开销最大。

真正含义

当 Flutter 重新 build 时:

  • Widget:一定是新建的(不可变,没法复用)。
  • Element & State:如果能匹配(位置相同 + 类型相同 + key 相同/都没有 key),就会复用旧的 Element 和 State,只替换其中的 widget 引用。
  • RenderObject:如果 widget 类型相同,通常也会复用原来的 RenderObject。

这里的“复用”指的是:复用 Element/State/RenderObject,而不是复用旧的 widget。

下面会结合案例进行说明。

没有 key 时的匹配规则

没有 key 时:

  • 匹配规则:按位置 + 类型
  • 如果新旧树 index 相同 且 widget 类型相同 → 复用旧的 Element/State/RenderObject
  • 问题:一旦顺序变化,就会错位复用,导致 UI/状态混乱。

案例

import 'package:flutter/material.dart';

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

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

  @override
  State<MyListDemo> createState() => _MyListDemoState();
}

class _MyListDemoState extends State<MyListDemo> {
  List<String> items = ["A", "B", "C"];

  void swapItems() {
    setState(() {
      items.insert(0, items.removeAt(2)); // 把最后一个移动到最前面
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("List Demo - No Key")),
      body: ListView(
        children: items.map((e) {
          return ListTile(
            title: Text(e),
            // ❌ 没有 key
          );
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: swapItems,
        child: const Icon(Icons.swap_horiz),
      ),
    );
  }
}

日志如下:

Restarted application in 3,540ms.
I/flutter (17988): Items after swap: [C, A, B]
I/flutter (17988): Items after swap: [B, C, A]
I/flutter (17988): Items after swap: [A, B, C]

flutter内部如何匹配和复用

初始状态(items = ["A", "B", "C"]

Widget Tree                Element Tree (持有State/RenderObject)
---------------------------------------------------------------
ListTile(Text("A"))   ──▶  Element[0]  ──▶  RenderObject("A")
ListTile(Text("B"))   ──▶  Element[1]  ──▶  RenderObject("B")
ListTile(Text("C"))   ──▶  Element[2]  ──▶  RenderObject("C")

调用 swapItems() 后(items = ["C", "A", "B"]

  • 新建的 Widget Tree:

    ListTile(Text("C"))   (index 0)
    ListTile(Text("A"))   (index 1)
    ListTile(Text("B"))   (index 2)
    
  • Flutter 匹配逻辑(没 key):

    • index 0:旧的 Element[0] 复用,但它之前绑定的是 "A" → ❌ 错乱
    • index 1:旧的 Element[1] 复用,但它之前绑定的是 "B" → ❌ 错乱
    • index 2:旧的 Element[2] 复用,但它之前绑定的是 "C" → ❌ 错乱
    Widget Tree (新)           Element Tree (旧的被复用,导致错位)
    ---------------------------------------------------------------
    ListTile(Text("C"))   ──▶  Element[0]  ──▶  RenderObject("A") (错)
    ListTile(Text("A"))   ──▶  Element[1]  ──▶  RenderObject("B") (错)
    ListTile(Text("B"))   ──▶  Element[2]  ──▶  RenderObject("C") (错)
    
  • 结果:

    • 无状态 widget(StatelessWidget) → 只是显示内容和预期不一致(轻微问题)。
      • 如上面案例:UI 没有正确变成 C, A, B,内容错乱。
    • 有状态 widget (StatefulWidget)→ State 被错位复用,数据/交互直接乱掉(严重问题)。
      • 典型 bug:输入框的内容“跑到”别的行;选中的复选框错位。

有 key 时的匹配规则

有 key 时:

  • 匹配规则:按 key + 类型
  • 如果新旧树 key 相同 且 widget 类型相同 → 复用旧的 Element/State/RenderObject
  • 即使顺序变化,也能正确找到对应 widget,不会错位。

案例

body: ListView(
  children: items.map((e) {
    return ListTile(
      key: ValueKey(e), // ✅ 每个 item 有唯一 key
      title: Text(e),
    );
  }).toList(),
),

日志如下:

I/flutter (22299): Items after swap: [C, A, B]
I/flutter (22299): Items after swap: [B, C, A]

flutter内部如何匹配和复用

如果给每个 item 一个 key: ValueKey(e),那么匹配逻辑变成 按 key 匹配

ListTile(Text("C"), key:"C") → 旧的 Element("C") → RenderObject("C")
ListTile(Text("A"), key:"A") → 旧的 Element("A") → RenderObject("A")
ListTile(Text("B"), key:"B") → 旧的 Element("B") → RenderObject("B")

这样匹配就正确,不会错位。UI 正确更新为:C, A, B ✅

Key 的几种类型

ValueKey<T>

直接用一个值作为标识。

常用于唯一 ID,比如列表项:

ListView(
  children: items.map((e) => Text(e.name, key: ValueKey(e.id))).toList(),
)

ObjectKey

用对象本身的 ==hashCode 来标识。

如果两个对象逻辑上相等(比如 id 相同),会被认为是同一个 widget。

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: ObjectKeyDemo()));

class Item {
  final int id;
  final String name;
  Item(this.id, this.name);

  @override
  bool operator ==(Object other) =>
      identical(this, other) || (other is Item && other.id == id);

  @override
  int get hashCode => id.hashCode;
}

class ObjectKeyDemo extends StatefulWidget {
  const ObjectKeyDemo({super.key});
  @override
  State<ObjectKeyDemo> createState() => _ObjectKeyDemoState();
}

class _ObjectKeyDemoState extends State<ObjectKeyDemo> {
  List<Item> items = [Item(1, "A"), Item(2, "B")];

  void swap() {
    setState(() {
      items.insert(0, items.removeAt(1)); // 交换顺序
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("ObjectKey Demo")),
      body: ListView(
        children: items.map((item) {
          return ListTile(
            key: ObjectKey(item), // ✅ 使用对象作为 key
            title: Text(item.name),
            trailing: SizedBox(
              // ✅ 给 TextField 约束宽度
              width: 150,
              child: TextField(
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "输入点内容",
                ),
              ),
            ),
          );
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: swap,
        child: const Icon(Icons.swap_horiz),
      ),
    );
  }
}

运行后,在输入框里输入点内容,然后点交换按钮:如果用 ObjectKey,状态不会错乱(输入框里的内容还在对应的 item 上)。

UniqueKey

每次创建都是全新的,永远不相等,不可以复用

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: UniqueKeyDemo()));

class UniqueKeyDemo extends StatefulWidget {
  const UniqueKeyDemo({super.key});
  @override
  State<UniqueKeyDemo> createState() => _UniqueKeyDemoState();
}

class _UniqueKeyDemoState extends State<UniqueKeyDemo> {
  List<String> items = ["A", "B"];

  void swap() {
    setState(() {
      items.insert(0, items.removeAt(1)); // 交换顺序
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("UniqueKey Demo")),
      body: ListView(
        children: items.map((e) {
          return ListTile(
            key: UniqueKey(), // ❌ 每次都是新的 key
            title: Text(e),
            trailing: SizedBox(
              // ✅ 给 TextField 约束宽度
              width: 150,
              child: TextField(
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "输入点内容",
                ),
              ),
            ),
          );
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: swap,
        child: const Icon(Icons.swap_horiz),
      ),
    );
  }
}

每次交换顺序,输入框的内容都会丢失,因为 UniqueKey 让 Flutter 认为这是全新的 widget,旧的 state 全部销毁。

GlobalKey

访问State

全局唯一,可以在 widget 树外访问 State

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: GlobalKeyDemo()));

class GlobalKeyDemo extends StatefulWidget {
  const GlobalKeyDemo({super.key});
  @override
  State<GlobalKeyDemo> createState() => _GlobalKeyDemoState();
}

class _GlobalKeyDemoState extends State<GlobalKeyDemo> {
  final GlobalKey<_CounterState> counterKey = GlobalKey<_CounterState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("GlobalKey Demo")),
      // ✅ 使用 GlobalKey 包裹子组件
      // key: counterKey : 指定 Counter 的 GlobalKey
      // 在这里key: counterKey,flutter会帮我们把这个key传递给Counter组件
      // state 会被正确地关联
      body: Center(child: Counter(key: counterKey)),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ✅ 直接通过 GlobalKey 访问子组件的 State
          // counterKey.currentState : 获取 Counter 的 State
          counterKey.currentState!.increment();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

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

  @override
  Widget build(BuildContext context) {
    return Text("Count是: $count", style: const TextStyle(fontSize: 24));
  }
}

运行后,点击右下角按钮,会直接修改子组件 Counter 的状态。这就是 GlobalKey 的强大之处:外部可以访问内部的 State

通过 GlobalKey获取FormState

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: ...
);

// 使用
_formKey.currentState!.validate();
_formKey.currentState!.save();

final _formKey = GlobalKey<FormState>();

  • _formKey 是一个 GlobalKey<FormState> 对象。
  • 它是一个标识符,类型参数 <FormState> 限定了它只能绑定 FormState 实例。

Form(key: _formKey, child: ...)

  • Form 被插入 widget 树时:
    • Flutter 调用 Form.createState(),创建一个新的 FormState 实例
    • 框架会把这个 FormState_formKey 关联起来。

_formKey.currentState

  • 返回与该 key 绑定的 FormState 实例。这就是 Form widget 的状态对象。

×

喜欢就点赞,疼爱就打赏