{
"title": "Flutter Key的使用",
"tags": [
"post",
"Flutter",
"Dart"
],
"sources": [
"xlog"
],
"external_urls": [
"https://kano.xlog.app/Flutter-Key-de-shi-yong"
],
"date_published": "2023-04-10T04:49:57.645Z",
"content": "我们之前已经使用过很多组件了,像`Container`、`Row`、或者`ElevatedButton`等Widget,使用频率还是比较多的\n但不知你有没有注意到,他们的构造函数中,都会有一个Key,作为构造器的属性,它通常作为第一个属性存在。\n\n> 在flutter中,Key可以标识一个唯一的组件,正如同其名,Key一半用来做唯一的标识\n\n我们之前没有使用这个Key,这时候就会有两种情况:\n\n1. 组件类型不同:比如`Container`和`SizedBox`,此时Flutter内部可以通过组件的类型区分,此时无需使用**key**\n2. 组件类型相同,比如由多个`Container`组成的数组,此时Flutter无法通过组件类型区分多个组件,此时就需要使用**Key**\n\n**默认情况下,如果没有定义key,flutter默认并不会自动创建key,而是使用widget的类型和index来调用。**\n\n我们可以通过一个案例来描述没有key造成的问题:\n\n```dart\nclass MyHomePage extends StatefulWidget {\n const MyHomePage({super.key});\n\n @override\n State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n List<Widget> list = [];\n @override\n void initState() {\n super.initState();\n list = [\n const Box(color: Colors.blue),\n const Box(color: Colors.yellow),\n const Box(color: Colors.red),\n ];\n }\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n appBar: AppBar(\n title: const Text('Title'),\n ),\n body: Center(\n child: Column(\n mainAxisAlignment: MainAxisAlignment.center,\n children: list,\n ),\n ),\n floatingActionButton: FloatingActionButton(\n onPressed: () {\n setState(() {\n list.shuffle(); //shuffle可以随机打乱此列表的元素\n });\n },\n child: const Icon(Icons.refresh),\n ),\n );\n }\n}\n\nclass Box extends StatefulWidget {\n final Color color;\n const Box({Key? key, required this.color}) : super(key: key);\n\n @override\n State<Box> createState() => _BoxState();\n}\n\nclass _BoxState extends State<Box> {\n int _count = 0;\n @override\n Widget build(BuildContext context) {\n return Container(\n width: 100,\n height: 100,\n color: Colors.transparent,\n child: ElevatedButton(\n style: ButtonStyle(\n backgroundColor: MaterialStateProperty.all(widget.color)),\n onPressed: () {\n setState(() {\n _count++;\n });\n },\n child: Text(\n \"$_count\",\n style: Theme.of(context).textTheme.headlineLarge,\n ),\n ),\n );\n }\n}\n```\n\n以上是一个StatefulWidget的例子,通过点击`floatingActionButton`打乱显示在屏幕上的三个Box的顺序,效果如下\n(图片)\n\n![1[1].gif](ipfs://bafkreihi6n7gdqoeicqkj2dydozfkv7fioxl5ykt6vrqar7t43dx2gknwy)\n\n**可以看到,虽然点击重排按钮,box表面上改变了顺序,但是里面的State,也就是数字的显示,并没有随之改变,这是因为Flutter识别到这三个Box是一样的,只会做stateless层面的改变,而State中的东西是不会动的,所以才会造成这种现象(说的有点不太好,之后补充)**\n\n这时候就需要使用key来帮助我们了:\n\n## Key作为唯一标识\n\nKey可以分为两大类:`GlobalKey`和`LocalKey`,顾名思义,Global是全局的,Local是本地的(非全局)\n\n```dart\n//GlobalKey可以共享给全局使用\nfinal GlobalKey _globalKey1 = GlobalKey();\nfinal GlobalKey _globalKey2 = GlobalKey();\nfinal GlobalKey _globalKey3 = GlobalKey();\n//GlobalKey 是非常昂贵的,需要合理使用\n\n//LocalKey\n//ValueKey是LocalKey\nconst Box(color: Colors.blue, key: ValueKey(1)),\n//ObjectKey也是LocalKey\nconst Box(color: Colors.red, key: ObjectKey(Text(\"key\"))),\n//UniqueKey也是LocalKey,作为独一无二的存在(生成一个具有唯一性的 hash 码)\nBox(color: Colors.red, key: UniqueKey()),\n//PageStorageKey可以当前保存页面的状态,例如列表的滚动状态\nPageStorageKey()\n```\n\n`LocalKey`与`GlobalKey`的区别:\n\n* `LocalKey`应用于有相同父节点的比较情况,也就是一个父节点管理多个子节点,子节点最好使用`LocalKey`\n* `GlobalKey`能够跨 Widget 访问状态,应用于有多个父节点的比较情况,也就是子节点对应多个父节点,比如下面会说的的横竖屏切换功能,需要在`Row`和`Colunm`中切换,如果只是用`LocalKey`的话,会丢失`State`现有的状态。\n\n以上是定义`GlobalKey`和`LocalKey`的方式。\n\n**现在让我们给上面实例的的Box加上Key:**\n\n*以下的案例会使用到`MediaQuery`中的`orientation`属性查询设备横竖屏状态*\n\n```dart\nclass MyHomePage extends StatefulWidget {\n const MyHomePage({super.key});\n\n @override\n State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n //GlobalKey,可以共享给全局,这样在横竖屏切换的时候,切换组件就不会丢失状态了\n final GlobalKey _globalKey1 = GlobalKey();\n final GlobalKey _globalKey2 = GlobalKey();\n final GlobalKey _globalKey3 = GlobalKey();\n List<Widget> list = [];\n @override\n void initState() {\n super.initState();\n list = [\n Box(color: Colors.blue, key: _globalKey1),\n Box(color: Colors.yellow, key: _globalKey2),\n Box(color: Colors.red, key: _globalKey3),\n ];\n }\n\n //我们使用localKey(ValueKey)的话,会丢失状态\n // List<Widget> list = [\n // const Box(color: Colors.blue, key: ValueKey(1)),\n // const Box(color: Colors.yellow, key: ValueKey(2)),\n // const Box(color: Colors.red, key: ValueKey(3)),\n // ];\n\n @override\n Widget build(BuildContext context) {\n //竖屏protrait横屏landscape\n print(MediaQuery.of(context).orientation);\n\n return Scaffold(\n appBar: AppBar(\n title: const Text('Title'),\n ),\n body: Center(\n //横屏横向显示,竖屏竖向显示\n //但是这样更改以后,状态就无法保存了,因为Column和Row的key还有组件本身就是不一样的\n child: MediaQuery.of(context).orientation == Orientation.portrait\n ? Column(\n mainAxisAlignment: MainAxisAlignment.center,\n children: list,\n )\n : Row(\n mainAxisAlignment: MainAxisAlignment.center,\n children: list,\n ),\n ),\n floatingActionButton: FloatingActionButton(\n onPressed: () {\n setState(() {\n list.shuffle(); //shuffle可以随机打乱此列表的元素\n });\n },\n child: const Icon(Icons.refresh),\n ),\n );\n }\n}\n```\n\n效果如下:\n![1-2[1].gif](ipfs://bafybeid5rlfh32mbiyzhyxst2lqdtgasdcss5ib266iua7nt467yqoajx4)\n\n\n> 注意:使用 `GlobalKey` 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 `GlobalKey` 在整个 widget 树中必须是唯一的,不能重复。\n\n## 使用GlobalKey操作子元素\n\n> GlobalKey有一个非常重要的特性:可以跨Widget访问State\n\n举个例子,如果想获取子Widget(Box)内的State,我们可以这样做\n\n```dart\nvar boxState = _globalKey.currentState as _BoxState;\n```\n\n拿到了`boxState`之后,就可以直接调用`BoxState`中的方法、属性了\n\n另外,我们还可以获取子widget节点\n\n```dart\nvar boxWidget = _globalKey.currentWidget as Box;\n```\n\n这样可以拿到box中的各种属性了,比如背景颜色,宽高,字体大小等\n\n完整演示:\n\n```dart\n//父widget\nclass MyHomePage extends StatefulWidget {\n const MyHomePage({super.key});\n\n @override\n State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n //GlobalKey,可以共享给全局\n final GlobalKey _globalKey = GlobalKey();\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n appBar: AppBar(\n title: const Text('Title'),\n ),\n body: Center(\n child: Box(color: Colors.blue, key: _globalKey),\n ),\n floatingActionButton: FloatingActionButton(\n onPressed: () {\n //父组件获取子元素的statewidget里的属性*\n var boxState = _globalKey.currentState as _BoxState;\n setState(() {\n boxState._count++;\n });\n print(boxState._count);\n //调用子Widget的方法\n boxState.run();\n\n //获取子widget\n var boxWidget = _globalKey.currentWidget as Box;\n print(boxWidget.color); //MaterialColor(primary value: Color(0xff2196f3))\n\n //获取子组件渲染的属性\n var renderBox =\n _globalKey.currentContext!.findRenderObject() as RenderBox;\n\n print(renderBox.size); // Size(100.0, 100.0)\n },\n child: const Icon(Icons.add),\n ),\n );\n }\n}\n\n//子widget\nclass Box extends StatefulWidget {\n final Color color;\n const Box({Key? key, required this.color}) : super(key: key);\n\n @override\n State<Box> createState() => _BoxState();\n}\n\nclass _BoxState extends State<Box> {\n int _count = 0;\n void run() {\n print(\"我是box的Run方法\");\n }\n\n @override\n Widget build(BuildContext context) {\n return Container(\n width: 100,\n height: 100,\n color: Colors.transparent,\n child: ElevatedButton(\n style: ButtonStyle(\n backgroundColor: MaterialStateProperty.all(widget.color)),\n onPressed: () {\n setState(() {\n _count++;\n });\n },\n child: Text(\n \"$_count\",\n style: Theme.of(context).textTheme.headlineLarge,\n ),\n ),\n );\n }\n}\n```\n\n",
"attributes": [
{
"value": "Flutter-Key-de-shi-yong",
"trait_type": "xlog_slug"
}
]
}