我们之前已经使用过很多组件了,像Container
、Row
、或者ElevatedButton
等 Widget,使用频率还是比较多的
但不知你有没有注意到,他们的构造函数中,都会有一个 Key,作为构造器的属性,它通常作为第一个属性存在。
在 flutter 中,Key 可以标识一个唯一的组件,正如同其名,Key 一半用来做唯一的标识
我们之前没有使用这个 Key,这时候就会有两种情况:
- 组件类型不同:比如
Container
和SizedBox
,此时 Flutter 内部可以通过组件的类型区分,此时无需使用key - 组件类型相同,比如由多个
Container
组成的数组,此时 Flutter 无法通过组件类型区分多个组件,此时就需要使用Key
默认情况下,如果没有定义 key,flutter 默认并不会自动创建 key,而是使用 widget 的类型和 index 来调用。
我们可以通过一个案例来描述没有 key 造成的问题:
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 是本地的(非全局)
//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
属性查询设备横竖屏状态
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,我们可以这样做
var boxState = _globalKey.currentState as _BoxState;
拿到了boxState
之后,就可以直接调用BoxState
中的方法、属性了
另外,我们还可以获取子 widget 节点
var boxWidget = _globalKey.currentWidget as Box;
这样可以拿到 box 中的各种属性了,比如背景颜色,宽高,字体大小等
完整演示:
//父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,
),
),
);
}
}