Flutter key

一,前言

在開發 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 的時候,將會只比較這兩個 WidgetruntimeType 。因爲兩個 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便可以在多個頁面或者層級複用,好比兩個頁面也可也同時保持一個狀態。

相關文章
相關標籤/搜索