Flutter之Key的原理解析

夜幕降臨耶發表於2021-04-08

前言

在Flutter開發中,會發現在很多的元件的建構函式中都會有一個可選的引數Key,你可能會疑惑這個Key的作用是什麼?這篇文章就來揭開Key的面紗。

1、案例解析

先不著急去看Key是什麼?我們先看一下下面這段程式碼的執行。

class _KeyDemoState extends State<KeyDemo> {
  List<Widget> itemList = [
    StatelessItem("A"),
    StatelessItem("B"),
    StatelessItem("C"),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("KeyDemo"),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: itemList,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            itemList.removeAt(0);
          });
        },
      ),
    );
  }
}

class StatelessItem extends StatelessWidget {
  final String _title;
  StatelessItem(this._title);
  final _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: _color,
      child: Text(_title),
      alignment: Alignment.center,
    );
  }
}
複製程式碼

其實這段程式碼很簡單,就是在介面上顯示三個不同顏色的Container,點選按鈕依次刪除第一個色塊。

filename.gif

如上所示結果正如我們所訴求的一樣,並無不妥,那麼如果我們將StatelessItem替換成StatefulWidget的widget又當如何呢?

class StatefulItem extends StatefulWidget {
  final String _title;
  StatefulItem(this._title);
  
  @override
  _StatefulItemState createState() => _StatefulItemState();
}

class _StatefulItemState extends State<StatefulItem> {
  final _color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: _color,
      child: Text(widget._title),
      alignment: Alignment.center,
    );
  }
}
複製程式碼

filename1.gif

正如上所示的效果,在操作的過程中出現了錯亂,本應該已刪除的第一個色塊的顏色顯示在了第二個色塊的部分。那麼為什麼會出現這樣的現象?這和Flutter的Widgetdiff更新機制有莫大的關係。

2、Widget更新

如果你對於Flutter有一定了解,那麼一定知道Flutter中的三棵樹Widget Tree,Element Tree,Render Tree共同鑄就Flutter的渲染流程,關於Flutter的渲染流程會在後續的文章中詳細介紹。

在Flutter中使用Widget來構建你的UI,Widget描述了他們的檢視在給定其當前配置和狀態時的樣式,當widget的狀態發生變化時,widget會重新構建UI。看起來重新構建UI的過程是重新渲染Widget樹的過程,實際上Widget只是想當於一個配置清單,Element才是被使用的物件,重新渲染檢視是對Element樹的渲染過程。為了效能上的考慮,重新渲染的先決條件是判斷兩個新老widget的runtimeTypekey是否一致。如果一致則說明不需要替換Element,直接更新widget就可以了。

  /// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
複製程式碼

2.1、StatelessItem的比較過程

在第一個程式碼裡面的Container是一個StatelessWidget的widget,同時也沒有傳入key到建構函式中,所以對於canUpdate方法而言,只需要比較新老widget的runtimeType即可,很明顯這裡的runtimeType是一致的,所以會返回true。當點選按鈕刪除第一個Container時,StatelessElement便會呼叫新持有Widget的build方法重新構建UI,所以便可以看到色塊是按順序刪除的。

2.2、StatefulItem的比較過程

同理在StatefulWidget的Container中,也沒有傳入key,所以對於canUpdate方法而言,只需要比較新老widget的runtimeType即可,很明顯這裡的runtimeType是一致的,所以會返回true。而不同的是顏色是在State中的,在點選按鈕刪除一個Container時,會重新build構建UI,但是不會刪除Element,只是從原持有的State例項中build新的widget。因為Element沒有變,所以State不會變化,那麼顏色也不會變化。

而如果給Widget一個key之後,canUpdate方法將會返回false,即表示需要重新構建Element。此時RenderObjectElement會用新Widget的key在老Element列表裡面查詢,找到匹配的則會更新Element的位置並更新對應renderObject的位置,對於這裡的程式碼而言就是刪除對應的Element。

image.png

整個過程如上圖所示,原本的Wiget和Element之間的關係如黑色連線所示,待刪除Widget A後,它們之間的關係便會如紅色連線所示。

傳入key後的效果如下:

List<Widget> itemList = [
    StatefulItem("A", key: ValueKey(1)),
    StatefulItem("B", key: ValueKey(2)),
    StatefulItem("C", key: ValueKey(3)),
  ];
複製程式碼

filename.gif

3、Key的分類

abstract class Key {
  const factory Key(String value) = ValueKey<String>;
  @protected
  const Key.empty();
}
複製程式碼

Key本身是一個抽象類,由此派生出兩種不同用途的Key:LocalKeyGlobalKey

3.1、LocalKey

LocalKey直接繼承自Key,是用作Widget重新整理的diff演算法的核心所在,用作Element和Widget進行比較。
LocalKey又派生出很多的子類:

  • ValueKey:以一個資料作為Key。如:數字、字元等;
  • ObjectKey:以Object物件作為Key;
  • UniqueKey:可以保證Key的唯一性;(一旦使用Uniquekey那麼就不存在Element複用 了!)

3.1、GlobalKey

GlobalKey可以用來標識唯一子widget。GlobalKey在整個widget結構中必須是全域性唯一的,而不像LocalKey只需要在兄弟widget中唯一。由於它們是全域性唯一的,因此可以使用GlobalKey來獲取到對應的Widget的State物件!

相關文章