banner
MiniKano

MiniKano

This is MiniKano's blog based on Blockchain technology. 新手,请多关照~
github

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 的順序,效果如下
(圖片)

1[1].gif

可以看到,雖然點擊重排按鈕,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),
            ),
        );
    }
}

效果如下:
1-2[1].gif

注意:使用 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,
        ),
      ),
    );
  }
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。