Flutter Key的使用

我们之前已经使用过很多组件了，像Container、Row、或者ElevatedButton等 Widget，使用频率还是比较多的
但不知你有没有注意到，他们的构造函数中，都会有一个 Key，作为构造器的属性，它通常作为第一个属性存在。

在 flutter 中，Key 可以标识一个唯一的组件，正如同其名，Key 一半用来做唯一的标识

我们之前没有使用这个 Key，这时候就会有两种情况：

组件类型不同：比如Container和SizedBox，此时 Flutter 内部可以通过组件的类型区分，此时无需使用key

组件类型相同，比如由多个Container组成的数组，此时 Flutter 无法通过组件类型区分多个组件，此时就需要使用Key

默认情况下，如果没有定义 key，flutter 默认并不会自动创建 key，而是使用 widget 的类型和 index 来调用。

我们可以通过一个案例来描述没有 key 造成的问题：

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

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
const Box(color: Colors.blue),
const Box(color: Colors.yellow),
const Box(color: Colors.red),
];
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); //shuffle可以随机打乱此列表的元素
});
},
child: const Icon(Icons.refresh),
),
);
}
}

class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);

@override
State<Box> createState() => _BoxState();
}

class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}

以上是一个 StatefulWidget 的例子，通过点击floatingActionButton打乱显示在屏幕上的三个 Box 的顺序，效果如下
(图片)

可以看到，虽然点击重排按钮，box 表面上改变了顺序，但是里面的 State，也就是数字的显示，并没有随之改变，这是因为 Flutter 识别到这三个 Box 是一样的，只会做 stateless 层面的改变，而 State 中的东西是不会动的，所以才会造成这种现象（说的有点不太好，之后补充）

这时候就需要使用 key 来帮助我们了：

Key 作为唯一标识

Key 可以分为两大类：GlobalKey和LocalKey，顾名思义，Global 是全局的，Local 是本地的（非全局）

Copy
//GlobalKey可以共享给全局使用
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
//GlobalKey 是非常昂贵的，需要合理使用

//LocalKey
//ValueKey是LocalKey
const Box(color: Colors.blue, key: ValueKey(1)),
//ObjectKey也是LocalKey
const Box(color: Colors.red, key: ObjectKey(Text("key"))),
//UniqueKey也是LocalKey,作为独一无二的存在（生成一个具有唯一性的 hash 码）
Box(color: Colors.red, key: UniqueKey()),
//PageStorageKey可以当前保存页面的状态，例如列表的滚动状态
PageStorageKey()

LocalKey与GlobalKey的区别：

LocalKey应用于有相同父节点的比较情况，也就是一个父节点管理多个子节点，子节点最好使用LocalKey

GlobalKey能够跨 Widget 访问状态，应用于有多个父节点的比较情况，也就是子节点对应多个父节点，比如下面会说的的横竖屏切换功能，需要在Row和Colunm中切换，如果只是用LocalKey的话，会丢失State现有的状态。

以上是定义GlobalKey和LocalKey的方式。

现在让我们给上面实例的的 Box 加上 Key：

以下的案例会使用到MediaQuery中的orientation属性查询设备横竖屏状态

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

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
//GlobalKey，可以共享给全局，这样在横竖屏切换的时候，切换组件就不会丢失状态了
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
Box(color: Colors.blue, key: _globalKey1),
Box(color: Colors.yellow, key: _globalKey2),
Box(color: Colors.red, key: _globalKey3),
];
}

//我们使用localKey（ValueKey）的话，会丢失状态
// List<Widget> list = [
// const Box(color: Colors.blue, key: ValueKey(1)),
// const Box(color: Colors.yellow, key: ValueKey(2)),
// const Box(color: Colors.red, key: ValueKey(3)),
// ];

@override
Widget build(BuildContext context) {
//竖屏protrait横屏landscape
print(MediaQuery.of(context).orientation);

return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
//横屏横向显示，竖屏竖向显示
//但是这样更改以后，状态就无法保存了，因为Column和Row的key还有组件本身就是不一样的
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); //shuffle可以随机打乱此列表的元素
});
},
child: const Icon(Icons.refresh),
),
);
}
}

效果如下：

注意：使用 GlobalKey 开销较大，如果有其他可选方案，应尽量避免使用它。另外，同一个 GlobalKey 在整个 widget 树中必须是唯一的，不能重复。

使用 GlobalKey 操作子元素

GlobalKey 有一个非常重要的特性：可以跨 Widget 访问 State

举个例子，如果想获取子 Widget（Box）内的 State，我们可以这样做

Copy
var boxState = _globalKey.currentState as _BoxState;

拿到了boxState之后，就可以直接调用BoxState中的方法、属性了

另外，我们还可以获取子 widget 节点

Copy
var boxWidget = _globalKey.currentWidget as Box;

这样可以拿到 box 中的各种属性了，比如背景颜色，宽高，字体大小等

完整演示：

Copy
//父widget
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
//GlobalKey，可以共享给全局
final GlobalKey _globalKey = GlobalKey();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Box(color: Colors.blue, key: _globalKey),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
//父组件获取子元素的statewidget里的属性*
var boxState = _globalKey.currentState as _BoxState;
setState(() {
boxState._count++;
});
print(boxState._count);
//调用子Widget的方法
boxState.run();

//获取子widget
var boxWidget = _globalKey.currentWidget as Box;
print(boxWidget.color); //MaterialColor(primary value: Color(0xff2196f3))

//获取子组件渲染的属性
var renderBox =
_globalKey.currentContext!.findRenderObject() as RenderBox;

print(renderBox.size); // Size(100.0, 100.0)
},
child: const Icon(Icons.add),
),
);
}
}

//子widget
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);

@override
State<Box> createState() => _BoxState();
}

class _BoxState extends State<Box> {
int _count = 0;
void run() {
print("我是box的Run方法");
}

@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}