前言
在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
,點選按鈕依次刪除第一個色塊。
如上所示結果正如我們所訴求的一樣,並無不妥,那麼如果我們將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,
);
}
}
複製程式碼
正如上所示的效果,在操作的過程中出現了錯亂,本應該已刪除的第一個色塊的顏色顯示在了第二個色塊的部分。那麼為什麼會出現這樣的現象?這和Flutter的Widget
的diff
更新機制有莫大的關係。
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的runtimeType
和key
是否一致。如果一致則說明不需要替換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。
整個過程如上圖所示,原本的Wiget和Element之間的關係如黑色連線所示,待刪除Widget A後,它們之間的關係便會如紅色連線所示。
傳入key後的效果如下:
List<Widget> itemList = [
StatefulItem("A", key: ValueKey(1)),
StatefulItem("B", key: ValueKey(2)),
StatefulItem("C", key: ValueKey(3)),
];
複製程式碼
3、Key的分類
abstract class Key {
const factory Key(String value) = ValueKey<String>;
@protected
const Key.empty();
}
複製程式碼
Key
本身是一個抽象類,由此派生出兩種不同用途的Key:LocalKey
和GlobalKey
。
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物件!