【Flutter】Widget的key是幹啥的

前言

以前入門一些Flutter應用的時候,老是會遇到GlobalKey這個類,當時我只從代碼的語法上感知到這個東西確定是用來綁定某些東西的,但至於key這東西是啥?爲何要綁定?不綁定的話會怎麼樣?爲何有的Widget實現須要綁定有有的不須要?這些通通都不知道。html

因而趁着端午有時間,就認真翻了下官方文檔,發現官方文檔說得很是詳細(前提是你對Flutter的控件樹有必定理解),上面的問題基本都回答到,惋惜的是官方是用視頻(YouToBe)講解的,這不便於忘記的時候速讀翻閱,因而我就整理成這篇博客順便加固下印象。java


key是什麼

key的做用是:控制weidget樹上的widget是否被替換(刷新)git

若是兩個weidget的runtimeTypekey屬性相等(用==比較),那麼本來指向舊weidge的element,它的指針會指向新的widget上(經過Element.update方法)。若是不相等,那麼舊element會從樹上移除,根據當前新的widget從新構建新element,並加到樹上指向新widget。github

咱們能夠看下代碼是否是這麼回事: Element.updateapi

@mustCallSuper
  void update(covariant Widget newWidget) {
    // This code is hot when hot reloading, so we try to
    // only call _AssertionError._evaluateAssertion once.
    assert(_lifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && Widget.canUpdate(widget, newWidget));
    // This Element was told to update and we can now release all the global key
    // reservations of forgotten children. We cannot do this earlier because the
    // forgotten children still represent global key duplications if the element
    // never updates (the forgotten children are not removed from the tree
    // until the call to update happens)
    assert(() {
      _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
      _debugForgottenChildrenWithGlobalKey.clear();
      return true;
    }());
    _widget = newWidget;
  }
複製代碼

進入上面的Widget.canUpdatemarkdown

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
複製代碼

能夠看到判斷邏輯基本與文檔一致,這裏有個值得注意的是:Widget自己不會調用Widget.canUpdate,這個方法是由Element負責調用的,也就是Widget能不能更新,最終仍是Element說了算app

相等時.png

不相等時.png

相信看到這裏你已經明白key是啥以及它的做用了,but talk is cheap show me the code,那麼咱們怎麼證實這理論是對的呢?下面就給出了代碼demo。less


何時會用到key

建一個demo先

下面先舉一個不須要用key的例子,代碼邏輯是,集合的元素順序變動後,控件要跟着變化,代碼以下:dom

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(home: PositionedTiles()));
}

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  List<Widget> tiles;

  @override
  void initState() {
    super.initState();
    tiles = [
      // StatefulColorfulTile(),
      // StatefulColorfulTile(),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatefulColorfulTile(key: UniqueKey()),
      StatelessColorfulTile(),
      StatelessColorfulTile(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Row(
          children: tiles,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        // child: Icon(Icons.sentiment_very_dissatisfied),
        onPressed: swapTiles,
      ),
    );
  }

  void swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}

// ignore: must_be_immutable
class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
        color: color,
        child: Padding(padding: EdgeInsets.all(70.0))
    );
  }
}

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  Color color;

  @override
  void initState() {
    super.initState();
    color = ColorUtil.randomColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: color,
        child: Padding(padding: EdgeInsets.all(70.0))
    );
  }
}

class ColorUtil {
  static Color randomColor() {
    var red = Random.secure().nextInt(255);
    var greed = Random.secure().nextInt(255);
    var blue = Random.secure().nextInt(255);
    return Color.fromARGB(255, red, greed, blue);
  }
}
複製代碼

上面的代碼效果以下,能夠看到使用StatelessColorfulTile時,點擊按鈕後兩個色塊能成功交換: QQ20210613173911HD.gifide


接下來咱們把代碼改爲下面這樣煮,重啓:

@override
  void initState() {
    super.initState();
    tiles = [
      StatefulColorfulTile(),
      StatefulColorfulTile(),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatelessColorfulTile(),
      // StatelessColorfulTile(),
    ];
  }
複製代碼

神奇的事情發生了,點擊按鈕後,色塊再也不發生交換: QQ20210613174103HD.gif

那在使用StatefulColorfulTile的前提下,如何讓色塊再次點擊按鈕後能發生交換呢?我猜聰明的你已經想到了,就是設置key屬性,即把代碼改爲下面這個樣子,重啓:

@override
  void initState() {
    super.initState();
    tiles = [
      // StatefulColorfulTile(),
      // StatefulColorfulTile(),
      StatefulColorfulTile(key: UniqueKey()),
      StatefulColorfulTile(key: UniqueKey()),
      // StatelessColorfulTile(),
      // StatelessColorfulTile(),
    ];
  }
複製代碼

效果以下:

QQ20210613172343HD.gif

接下來就是圖解形成這些效果的緣由了。


爲啥StatelessColorfulTile能交換

咱們先來看看StatelessColorfulTile交換的時候都發生了什麼,先來看看交換前的:

image.png

交換後的:

image.png

當代碼調用PositionedTiles.setState交換兩個Widget後,flutter會從上到下逐一對比Widget樹和Element樹中的每一個節點,若是發現節點的runtimeType和key一致的話(這裏沒有key,所以只對比runtimeType),那麼就認爲該Element仍然是有效的,可用複用,因而只須要更改Element的指針,就能夠直接複用。

而因爲StatefulColorfulTile的顏色信息是存儲在widget中的:

class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();
  
   ...(略)
}
複製代碼

因此即使色塊Widget由於Widget.canUpdate返回不須要更新,內部沒有回調到setState邏輯,也會成功交換。

Element保存了Widget和RenderObject,Widget是負責描述控件樣式,RenderObject則是佈局渲染控制,當Element只更新了Widget,下一次渲染時就會變成新Widget的效果了。


爲啥StatefulColorfulTile要加key才能交換

先從代碼的最表面說說StatefulColorfulTileStatelessColorfulTile一個重大的區別,即Color的屬性放的位置不同。

StatelessColorfulTile的Color屬性是直接放置在Widget下的:

class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();
  
   ...(略)
}
複製代碼

StatefulColorfulTile的Color屬性是放在State下的: image.png

這裏補充一個基礎知識,即State屬性,最終都會被Element管理,下面能夠簡單追幾段源碼看看。

首先看看StateFulWidget的抽象方法: image.png

有了Flutter三棵樹概念之後,咱們應該明白每一個Widget最終都會被建立出對應的Element,而建立的方法正是上面的createElement,它會調用StatefulElement構造函數來構造。

接着跟進StatefulElement()函數,咱們就能清晰地看到StatefulElement管理了State,而且拿它來作各類各樣的事了: image.png

明確了State屬性,最終都會被Element管理這個大前提後,接下來就好辦了。


咱們先來看看StatefulColorfulTile不帶key的時候,調用交換函數究竟發生了什麼,依舊是先看交換前的: image.png

交換後的: image.png

相信緣由不用我多說了,首先仍是Widget更新後,flutter會根據runtimeTypekey比較Widget從而判斷是否須要從新構建Element,這裏key爲空,只比較runtimeType,比較結果必然相等,因此Element直接複用。

StatefulColorfulTile在從新渲染時,Color屬性再也不是從Widget對象(即自身)裏獲取,而是從ElementState裏面獲取,而Element根本沒發生變化,因此取到的Color也沒有變化,最終就算怎麼渲染,顏色都是不變的,視覺效果上也就是兩個色塊沒有交換了。


接着看有了key以後,交換前:

image.png

交換後,發現兩邊key不相等,因而嘗試匹配Element是否還有相同的id,發現有,因而從新排列Element讓相同key的配對: image.png

若是Element這邊沒有key能與新Widget匹配得上,那麼舊的Element會失效,後續根據新Widget從新構建一個Element。

rebuild後,Element已改變,從新渲染後視覺上就看到兩個色塊交換位置了: image.png

熟悉三棵樹原理的咱們知道,Element就至關於設備上的真實控件,既然Element的位置變化了,那麼最終屏幕上的控件也就跟着變化了,最終交換後從新渲染給視覺上就是兩個色塊交換了。

好了,本篇博客先到這裏結束了,這裏只是簡單介紹了下Widget中key的做用,但實際上Key還有不少種實現,他們用處各有不一樣,這個由於和本篇目標沒啥太大關係,因此不介紹了,有空本身翻翻官方文檔其實很快也能搞懂了。

相關文章
相關標籤/搜索