一,前言
在開發 Flutter 的過程當中你可能會發現,一些小部件的構造函數中都有一個可選的參數——Key。在這篇文章中咱們會深刻淺出的介紹什麼是 Key,以及應該使用 key 的具體場景。算法
二,什麼是Key
在 Flutter 中咱們常常與狀態打交道。咱們知道 Widget 能夠有 Stateful 和 Stateless 兩種。Key 可以幫助開發者在 Widget tree 中保存狀態,在通常的狀況下,咱們並不須要使用 Key。那麼,究竟何時應該使用 Key呢。less
咱們來看看下面這個例子。dom
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 初始化一個隨機顏色。ide
咱們如今將這個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 中。ui
如今咱們仍是使用剛纔同樣的佈局,只不過把 StatelessContainer 替換成 StatefulContainer,看看會發生什麼。this
這時,不管咱們怎樣點擊,都再也沒有辦法交換這兩個Container的順序了,而 switchWidget 確實是被執行了的。spa
爲了解決這個問題,咱們在兩個 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便可以在多個頁面或者層級複用,好比兩個頁面也可也同時保持一個狀態。