Flutter學習筆記:Key的作用

TonyBuilder發表於2019-09-01

  Flutter 框架中 Widget 建構函式都提供了Key 作為可選引數。

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);
複製程式碼
abstract class StatefulWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatefulWidget({ Key key }) : super(key: key);
複製程式碼

  Key 是一個比較難以理解的概念,官方指導視訊When to Use Keys - Flutter Widgets 101 Ep. 4 對此做了簡要的介紹。

  但是,在看過這個視訊之後,部分同學的主要疑問在於,例子中為什麼 StatefulWidget必須要使用Key才能更換顏色,而StateleesWidget不需要。

Talk is cheap, show me the code.

  對於這個問題,我們需要跟蹤原始碼獲得更準確的答案。

問題複述

  Demo 視訊中建立了一個 Widget tree, 包含兩個隨機顏色的色塊。 如果是 StatelessWidget 構建的Widget,點選按鈕就可以調換順序; 但是如果是StatefulWidget,就不能更換顏色。 為了解決這個問題,我們可以在StatefulWidget構造時傳入一個UniqueKey, 就可以使得Demo中顏色發生變化。

Demo 程式碼路徑: flutter_keys

問題分析

  理解這個問題的為什麼會發生,需要有一些 Flutter framework 的基礎知識。

Widget Tree, Element Tree 和 Render Tree

  Flutter framework 維護了三棵樹來描述應用程式的UI;

  • Widget Tree

    由 Widget 組成的配置樹,提供給開發人員作為描述UI結構的程式設計介面,是一個輕量級 的樹。例如案例中的 Widget Tree可以描述為:

widget_tree

  • Render Tree,

    由RenderObject組成的樹,每一個 RenderObject 都包含了 layout, paint, hitTest 等功能,具備實際的佈局,繪製,按鍵檢測等功能,因此是一個重量級的樹。Demo中對應的 Render Tree 為

Flutter學習筆記:Key的作用

  • Element Tree,

    為了便於開發人員更好的控制Render Tree, 介於 Widget Tree, 和 Render Tree之間, Flutter Framework 生成了 Element Tree。 Element Tree 由 Widget提供的配置資訊生成。

樹的生成和重新整理

  在Flutter 應用第一次啟動的時候,attachRootWidget 方法生成了這三棵樹。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
複製程式碼

  在使用者點選按鈕時,我們通過SetState() 設定相關element節點狀態為dirty, 從而在下一個vsync訊號 到來時觸發控制元件樹的重新整理。

  switchWidget() {
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
複製程式碼
  @protected
  void setState(VoidCallback fn) {
    ......
    _element.markNeedsBuild();
  }
複製程式碼

StatelessWidget element 的生成

  在重新整理過程中,StatelessWidget 會生成 StatelessElement:

abstract class StatelessWidget extends Widget {
  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  @override
  StatelessElement createElement() => StatelessElement(this);
複製程式碼

  StatelessElement 在執行update 操作時,rebuild() 最終會呼叫到 build()方法從而直接 重建一個Widget.

  可以看到 StatelessElement,僅持有一個對Widget的引用,沒有狀態資訊,這就是Stateless本義。

/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

複製程式碼

StatefulWidget element 的生成

  相應的,重新整理過程中,StatefulWidget 會生成 StatefulElement,此Element除了持有一個widget引用, 還持有了一個State的引用,新建element的時候儲存在 _state 變數中。

class StatefulElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
複製程式碼

  使用者點選了按鈕,觸發了element tree 的重新整理後,呼叫StatefulElement.update進行更新。 但是,StatefulElement此時僅更新了widget的引用,沒有更新state。

  @override
  void update(StatefulWidget newWidget) {
    ......
    _state._widget = widget;
複製程式碼

  因此,Element.rebuid 最終會呼叫到我們定義的_StatefulContainerState.build中時, color 還是之前的值,並沒有變化。

  因為 _state沒有更新,還是StatefulElement構造時使用的那個。

問題解決方案:使用 Key 標記 Element

  Flutter 提出解決 StatefulElement 如何更新 state 的方案, 就是使用 Key 標記 Element。發現 Key 不相同時重新重新整理Element。

Element Tree 觸發更新過程:

  Key 方案是如何生效的?首先我們回顧上面 Element Tree更新的過程

  • 從根結點開始,Element.updateChild

  • 深度遍歷到Row節點, child 是 StatefulContainer

  • 判斷 Widget.canUpdate(child.widget, newWidget) 是true,可以執行更新

    這裡 child.widget 是變化之前的 StatefulContainer, newWidget 是新的StatefulContainer;

  • 由於上一步判斷可以更新,因此執行 child.update(newWidget);也就是 StatefulElement.update

  • StatefulElement.update的時候,僅更新了widget的引用,沒有更新state,導致顏色沒有變化。

  @override
  void update(StatefulWidget newWidget) {
    ......
    _state._widget = widget;
複製程式碼

  因此,解決方案的關鍵在於 Widget.canUpdate 需要返回 false。

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
複製程式碼

  為此Flutter設計人員設計了這個方法:當我們給 StatefulContainer 設定 Key 時,Widget.canUpdate 返回 false。 原有的Element會被暫時deactive,在需要 時重新mount到樹上。從而保證 StatefulWidget 狀態可更新,而且狀態不丟失。

總結

Flutter學習筆記:Key的作用

  一圖省千言,其實本文就是為了解釋視訊中的這張圖,從圖中可以看到,Widget和State狀態對應產生了問題。Key 就是為了解決圖中widget更新而state沒有更新導致的問題而出現的。

  限於篇幅,本文僅分析了不使用Key導致state和widget對應錯誤的問題。 還有一種場景是,使用了key,但是導致State被重新建立從而丟失State資訊。這就是Key的作用域的問題。 對應的解決方案就是 GloableKey,留作後續分析。

相關文章