Widget 的 Key(一)


在〈第一個 Flutter 專案〉中看到,範例在繼承 StatefulWidget 後,定義了 key,在該專案中其實沒用處,那麼 key 用在哪呢?

要先說結論的話,Flutter 的 Widget 會有結構相對應的 Element 樹,若希望 Widget 樹節點調整位置後,Element 樹也跟著調整對應的節點位置,就要使用 key

來個簡單範例好了,假設你想開發一個便利貼之類的 App,首先你開發了個原型,可以點選按鈕後,將目前管理的便利貼輪流顯示出來:

import 'package:flutter/material.dart';

void main() => runApp(MyNote());

class MyNote extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Note',
      home: MyHomePage(title: 'My Note Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({this.title});

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class Note extends StatelessWidget {
  Color color;
  double width;
  double height;

  Note({this.color, this.width, this.height});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: width,
      height: height,
    );
  }
}

class _MyHomePageState extends State<MyHomePage> {
  final notes = [
    Note(
      color: Colors.red,
      width: 200,
      height: 200,
    ),
    Note(
      color: Colors.blue,
      width: 200,
      height: 200,
    )
  ];

  void _round_robin() {
    setState(() {
      notes.add(notes.removeAt(0));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Stack(
          children: notes,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _round_robin,
        tooltip: 'round robin',
        child: Icon(Icons.swap_vert),
      ),
    );
  }
}

因為目前是原型,Note 暫且先繼承 StatelessWidget 定義,目前 _MyHomePageState 有兩個 Note,執行結果如下:

Widget 的 Key(一)

Stack 可以管理一組元件,以堆疊方式來繪製畫面,每次按下按鈕,調整的是 notes 中的元件順序,這個順序就是堆疊的順序,因為目前只有兩個 Note,結果就是兩個顏色輪流出現。

在首次執行時,Widget 樹與 Element 樹是這樣的:

Widget 的 Key(一)

以上只顯示了 Stack 子樹的部份,Element 樹與 Widget 樹結構是對應的,StatelessWidgetcreateElement 建立的,是 Element 的子類 StatelessElement 實例,虛線表示 Element 實例參考至 Widget 實例,Note 對應的 Element 是從 NotecreateElement 得來,使用對應顏色的外框表示從哪個 Note 建構而來。

接著你按下按鈕後,notes 的順序調整後重新 buildWidget 樹與 Element 樹一開始會是如下:

Widget 的 Key(一)

接著 Flutter 會以目前 Widget 樹結構與既有的 Element 樹結構比對,看看對應位置的 Widget 是否可用來更新 Element,這是由 WidgetcanUpdate 方法決定,其原始碼為:

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

Element 目前參考的 WidgetoldWidget,對應位置的 WidgetnewWidget,可否用來更新 Element 的意思是,Element 是否參考至 newWidget

Element 會以參考的 Widget 組態來建立畫面,從原始碼中可以看到,根據的是 WidgetruntimeTypekey 來決定傳回 truefalse,傳回 true 的話,Element 就會參考至 newWidget

在沒有設置 key 的情況下,keynull,也就是只要 runtimeType 相同,也就是 Widget 只要是同一型態,就可以更新 Element,就目前的範例來說就是如此,接下來兩棵樹會長這樣:

Widget 的 Key(一)

也就是說,Element 樹結構並沒有調整,只不過由藍色 Note 建立的 Element,現在參考至紅色的 Note,而紅色 Note 建立的 Element,現在參考至藍色的 Note,因為 Element 參考的 Widget 組態會被用來建立畫面,因此還是看到了顏色的變化。

對這個範例來說,因為 NoteStatelessWidgetElement 沒有保留狀態的問題,Element 只是以新的 Widget 組態來呈現畫面就沒有問題。

如果真的想令 Element 樹也調整,可以設置 Widget 有不同的 key,例如:

import 'package:flutter/material.dart';

void main() => runApp(MyNote());

...略

class Note extends StatelessWidget {
  Color color;
  double width;
  double height;

  // 設置 key
  Note({Key key, this.color, this.width, this.height}) : super(key: key);

  @override
  StatelessElement createElement() {
    return super.createElement();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: width,
      height: height,
    );
  }
}

class _MyHomePageState extends State<MyHomePage> {
  final notes = [
    Note(
      key: UniqueKey(),   // 設置 key
      color: Colors.red,
      width: 200,
      height: 200,
    ),
    Note(
      key: UniqueKey(),   // 設置 key
      color: Colors.blue,
      width: 200,
      height: 200,
    )
  ];

  ...略
}

在以上的範例中,兩個 Notekey 會不同,在調整了 Widget 樹前,本來是這樣:

Widget 的 Key(一)

接著按下按鈕後,Widget 樹變化了:

Widget 的 Key(一)

接著 Flutter 會以目前 Widget 樹結構與既有的 Element 樹結構比對,這時因為 canUpdate 傳回 false,不能直接以 newWidget 來更新 Element

Widget 的 Key(一)

因此 Flutter 會根據 key 查找 Element,將 Element 樹重新調整為符合目前 Widget 樹的結構:

Widget 的 Key(一)

設定 key 後的執行結果,就畫面效果而言相同,基本上,對於 StatelessWidget 不用設置 key,不過,藉由以上可以知道,key 的設置會影響 Element 樹與 Widget 之間的調整方式,而這個調整方式對於 StatefulWidget 就會有影響了,這下一篇文件再來談。