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可以描述為:
-
Render Tree,
由RenderObject組成的樹,每一個 RenderObject 都包含了 layout, paint, hitTest 等功能,具備實際的佈局,繪製,按鍵檢測等功能,因此是一個重量級的樹。Demo中對應的 Render Tree 為
-
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 狀態可更新,而且狀態不丟失。
總結
一圖省千言,其實本文就是為了解釋視訊中的這張圖,從圖中可以看到,Widget和State狀態對應產生了問題。Key 就是為了解決圖中widget更新而state沒有更新導致的問題而出現的。
限於篇幅,本文僅分析了不使用Key導致state和widget對應錯誤的問題。 還有一種場景是,使用了key,但是導致State被重新建立從而丟失State資訊。這就是Key的作用域的問題。 對應的解決方案就是 GloableKey,留作後續分析。