一,前言
在開發 Flutter 的過程中你可能會發現,一些小部件的構造函數中都有一個可選的參數——Key。在這篇文章中我們會深入淺出的介紹什么是 Key,以及應該使用 key 的具體場景。
二,什么是Key
在 Flutter 中我們經常與狀態打交道。我們知道 Widget 可以有 Stateful 和 Stateless 兩種。Key 能夠幫助開發者在 Widget tree 中保存狀態,在一般的情況下,我們並不需要使用 Key。那么,究竟什么時候應該使用 Key呢。
我們來看看下面這個例子。
class StatelessContainer extends StatelessWidget { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
這是一個很簡單的 Stateless Widget,顯示在界面上的就是一個 100 * 100 的有顏色的 Container。 RandomColor 能夠為這個 Widget 初始化一個隨機顏色。
我們現在將這個Widget展示到界面上。
class Screen extends StatefulWidget { @override _ScreenState createState() => _ScreenState(); } class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatelessContainer(), StatelessContainer(), ]; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: widgets, ), ), floatingActionButton: FloatingActionButton( onPressed: switchWidget, child: Icon(Icons.undo), ), ); } switchWidget(){ widgets.insert(0, widgets.removeAt(1)); setState(() {}); } }
這里在屏幕中心展示了兩個 StatelessContainer 小部件,當我們點擊 floatingActionButton 時,將會執行 switchWidget 並交換它們的順序。
看上去並沒有什么問題,交換操作被正確執行了。現在我們做一點小小的改動,將這個 StatelessContainer 升級為 StatefulContainer。
class StatefulContainer extends StatefulWidget { StatefulContainer({Key key}) : super(key: key); @override _StatefulContainerState createState() => _StatefulContainerState(); } class _StatefulContainerState extends State<StatefulContainer> { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
在 StatefulContainer 中,我們將定義 Color 和 build方法都放進了 State 中。
現在我們還是使用剛才一樣的布局,只不過把 StatelessContainer 替換成 StatefulContainer,看看會發生什么。
這時,無論我們怎樣點擊,都再也沒有辦法交換這兩個Container的順序了,而 switchWidget 確實是被執行了的。
為了解決這個問題,我們在兩個 Widget 構造的時候給它傳入一個 UniqueKey。
class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatefulContainer(key: UniqueKey(),), StatefulContainer(key: UniqueKey(),), ]; ···
然后這兩個 Widget 又可以正常被交換順序了。
看到這里大家肯定心中會有疑問,為什么 Stateful Widget 無法正常交換順序,加上了 Key 之后就可以了,在這之中到底發生了什么? 為了弄明白這個問題,我們將涉及 Widget 的 diff 更新機制。
-
Widget 更新機制
下面來來看Widget的源碼。@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; ··· static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }
我們知道 Widget 只是一個配置且無法修改,而 Element 才是真正被使用的對象,並可以修改。當新的 Widget 到來時將會調用 canUpdate 方法,來確定這個 Element是否需要更新。
canUpdate 對兩個(新老) Widget 的 runtimeType 和 key 進行比較,從而判斷出當前的 Element 是否需要更新。
-
-
StatelessContainer 比較過程
在 StatelessContainer 中,我們並沒有傳入 key ,所以只比較它們的 runtimeType。我們將 color 屬性定義在了 Widget 中,這將導致他們具有不同的 runtimeType。所以在 StatelessContainer 這個例子中,Flutter能夠正確的交換它們的位置。
-
-
-
StatefulContainer 比較過程
而在 StatefulContainer 的例子中,我們將 color 的定義放在了 State 中,Widget 並不保存 State,真正 hold State 的引用的是 Stateful Element。當我們沒有給 Widget 任何 key 的時候,將會只比較這兩個 Widget 的 runtimeType 。由於兩個 Widget 的屬性和方法都相同,canUpdate 方法將會返回 false,在 Flutter 看來,並沒有發生變化。所以這兩個 Element 將不會交換位置。而我們給 Widget 一個 key 之后,canUpdate 方法將會比較兩個 Widget 的 runtimeType 以及 key。並返回 true,現在 Flutter 就可以正確的感知到兩個 Widget 交換了順序了。 (這里 runtimeType 相同,key 不同)
-
-
- 總結:
我們在構建Flutter的UI時是以Widget的形式『拼接』出來的,組件樹作為UI每一個組件都對應一個元素(原文中是Slot),從而形成了『元素樹』(Element Tree),元素樹的內容非常簡單,只包含了組件的類型和子元素的引用(Type),你可以把元素樹當做Flutter App中的骨架(skeleton),它只展現了App的結構,並不包含其他具體的信息。 當我們交換組件樹中的元素時,組件確實進行了交換,但是元素樹卻不一定。Flutter會先遍歷(walk)整個元素樹,從Row上的主元素,到主元素的子元素,查看整體的結構是否發生了變化,當然,它檢查的只能是元素的Type和Key,在給出的例子中,當我們不設置Key時,元素樹對比Type,發現Type並沒有發生變化,而Flutter卻是用元素樹和元素對應的狀態(可用或者不可用),來決定這個元素是否應該顯示出來,所以在界面中並沒有發生改變,但是當我們加入Key之后,對比的對象多了一個,並且是和之前不一樣的,Flutter察覺到之后,立即改變了元素的狀態,讓它變為『無用狀態』(deactivate),當遍歷完之后,Flutter會瀏覽(look through)這些不匹配的元素(non-matched children)通過相應的引用為之找到對應的組件。當所有的元素都匹配完成之后,Flutter會刷新界面,展現出我們預想的。
- 總結:
-
比較范圍
為了提升性能 Flutter 的比較算法(diff)是有范圍的,它並不是對第一個 StatefulWidget 進行比較,而是對某一個層級的 Widget 進行比較。··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), ]; ···
在這個例子中,我們將兩個帶 key 的 StatefulContainer 包裹上 Padding 組件,然后點擊交換按鈕,會發生下面這件奇妙的事情。
結論:兩個 Widget 的 Element 並不是交換順序,而是被重新創建了。
分析:(1)我們分析一下這次的Widget Tree 和 Element Tree,當我們交換元素后,Flutter element-to-widget matching algorithm,(元素-組件匹配算法),開始進行對比,算法每次只對比一層,即Padding這一層。顯然,Padding並沒有發生本質的變化。
(2)於是開始進行第二層對比,在對比時Flutter發現元素與組件的Key並不匹配,於是,把它設置成不可用狀態,但是這里所使用的Key只是本地Key(Local Key),Flutter並不能找到另一層里面的Key(即另外一個Padding Widget中的Key)所以,Flutter就創建了一個新的Widget,而這個Widget的顏色就成了我們看到的『隨機色』。
總結:
所以為了解決這個問題,我們需要將 key 放到 Row 的 children 這一層級。
··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), ]; ···
現在我們又可以愉快的玩耍了(交換 Widget 順序)了。
三,Key 的種類
-
Key
@immutable abstract class Key { const factory Key(String value) = ValueKey<String>; @protected const Key.empty(); }
默認創建 Key 將會通過工廠方法根據傳入的 value 創建一個 ValueKey。
Key 派生出兩種不同用途的 Key:LocalKey 和 GlobalKey。
-
Localkey
LocalKey 直接繼承至 Key,它應用於擁有相同父 Element 的小部件進行比較的情況,也就是上述例子中,有一個多子 Widget 中需要對它的子 widget 進行移動處理,這時候你應該使用Localkey。
Localkey 派生出了許多子類 key:
-
- ValueKey : ValueKey('String')
- ObjectKey : ObjectKey(Object)
- UniqueKey : UniqueKey()
Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')
-
GlobalKey
@optionalTypeArgs abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ··· static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}; static final Set<Element> _debugIllFatedElements = HashSet<Element>(); static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{}; ··· BuildContext get currentContext ··· Widget get currentWidget ··· T get currentState ···
GlobalKey 使用了一個靜態常量 Map 來保存它對應的 Element。你可以通過 GlobalKey 找到持有該GlobalKey的 Widget,State 和 Element。
注意:GlobalKey 是非常昂貴的,需要謹慎使用。
四,什么時候需要使用 Key
-
ValueKey
如果您有一個 Todo List 應用程序,它將會記錄你需要完成的事情。我們假設每個 Todo 事情都各不相同,而你想要對每個 Todo 進行滑動刪除操作。
這時候就需要使用 ValueKey
return TodoItem( key: ValueKey(todo.task), todo: todo, onDismissed: (direction){ _removeTodo(context, todo); }, );
-
ObjectKey
如果你有一個生日應用,它可以記錄某個人的生日,並用列表顯示出來,同樣的還是需要有一個滑動刪除操作。
我們知道人名可能會重復,這時候你無法保證給 Key 的值每次都會不同。但是,當人名和生日組合起來的 Object 將具有唯一性。
這時候你需要使用 ObjectKey!
-
UniqueKey
如果組合的 Object 都無法滿足唯一性的時候,你想要確保每一個 Key 都具有唯一性。那么,你可以使用 UniqueKey。它將會通過該對象生成一個具有唯一性的 hash 碼。
不過這樣做,每次 Widget 被構建時都會去重新生成一個新的 UniqueKey,失去了一致性。也就是說你的小部件還是會改變。(還不如不用?)
-
PageStorageKey
當你有一個滑動列表,你通過某一個 Item 跳轉到了一個新的頁面,當你返回之前的列表頁面時,你發現滑動的距離回到了頂部。這時候,給 Sliver 一個 PageStorageKey 它將能夠保持 Sliver 的滾動狀態。
-
GlobalKey
GlobalKey 能夠跨 Widget 訪問狀態。 在這里我們有一個 Switcher 小部件,它可以通過 changeState 改變它的狀態。class SwitcherScreenState extends State<SwitcherScreen> { bool isActive = false; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Switch.adaptive( value: isActive, onChanged: (bool currentStatus) { isActive = currentStatus; setState(() {}); }), ), ); } changeState() { isActive = !isActive; setState(() {}); } }
但是我們想要在外部改變該狀態,這時候就需要使用 GlobalKey。
class _ScreenState extends State<Screen> { final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>(); @override Widget build(BuildContext context) { return Scaffold( body: SwitcherScreen( key: key, ), floatingActionButton: FloatingActionButton(onPressed: () { key.currentState.changeState(); }), ); } }
這里我們通過定義了一個 GlobalKey<SwitcherScreenState> 並傳遞給 SwitcherScreen。然后我們便可以通過這個 key 拿到它所綁定的 SwitcherState 並在外部調用 changeState 改變狀態了。
五,總結:
上面的例子,因為沒有數據,所以使用了UniqueKey,在真實的開發中,我們可以用Model中的id作為ObjectKey。
GlobalKey其實是對應於LocalKey,上面我們說Padding中的就是LocalKey,Global即可以在多個頁面或者層級復用,比如兩個頁面也可也同時保持一個狀態。