LOADING

Flutter Key 的使用

Flutter Key

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

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

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

  1. 组件类型不同:比如ContainerSizedBox,此时Flutter内部可以通过组件的类型区分,此时无需使用key
  2. 组件类型相同,比如由多个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可以分为两大类:GlobalKeyLocalKey,顾名思义,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()

LocalKeyGlobalKey的区别:

  • LocalKey应用于有相同父节点的比较情况,也就是一个父节点管理多个子节点,子节点最好使用LocalKey
  • GlobalKey能够跨 Widget 访问状态,应用于有多个父节点的比较情况,也就是子节点对应多个父节点,比如下面会说的的横竖屏切换功能,需要在RowColunm中切换,如果只是用LocalKey的话,会丢失State现有的状态。

以上是定义GlobalKeyLocalKey的方式。

现在让我们给上面实例的的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,
        ),
      ),
    );
  }
}

    发表回复

    电子邮件地址不会被公开。必填项已用 * 标注