【Flutter學習】之深入淺出 Key


一,前言

在開發 Flutter 的過程中你可能會發現,一些小部件的構造函數中都有一個可選的參數——Key。在這篇文章中我們會深入淺出的介紹什么是 Key,以及應該使用 key 的具體場景。

二,什么是Key

在 Flutter 中我們經常與狀態打交道。我們知道 Widget 可以有 StatefulStateless 兩種。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 中,我們將定義 Colorbuild方法都放進了 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 的 runtimeTypekey 進行比較,從而判斷出當前的 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:LocalKeyGlobalKey

  

  • 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即可以在多個頁面或者層級復用,比如兩個頁面也可也同時保持一個狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM