Note-52336-5

Token ID: 1

ERC-721 1 Transfers

Metadata

{
  "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"
    }
  ]
}