Flutter中Widget之key原理探索

開始

在Flutter的每個(gè)Widget中, 都會有key這個(gè)可選屬性. 在剛開始學(xué)習(xí)flutter時(shí), 基本就直接忽略不管了. 對執(zhí)行結(jié)果好像也沒什么影響. 現(xiàn)在來深究下key到底有什么作用.(研究一天時(shí)間, 發(fā)現(xiàn)key沒什么作用. 快要放棄, 又多寫了幾個(gè)簡單例子, 終于發(fā)現(xiàn)差異~~)

官方文檔介紹

key用于控制控件如何取代樹中的另一個(gè)控件.

如果2個(gè)控件的runtimeTypekey屬性operator==, 那么新的控件通過更新底層元素來替換舊的控件(通過調(diào)用Element.update). 否則舊的控件將從樹上刪除, element會生成新的控件, 然后新的element會被插入到樹中.

另外, 使用GlobalKey做為控件的key, 允許element在樹周圍移動(改變父節(jié)點(diǎn)), 而不會丟失狀態(tài). 當(dāng)發(fā)現(xiàn)一個(gè)新的控件時(shí)(它的key和類型與同一位置上控件不匹配), 但是在前面的結(jié)構(gòu)中有一個(gè)帶有相同key的小部件, 那么這個(gè)控件將會被移動到新的位置.

GlobalKey是很昂貴的. 如果不需要使用上述特性, 可以考慮使用Key, ValueKeyUniqueKey替換.

通常, 只有一個(gè)子節(jié)點(diǎn)的widget不需要指定key.

實(shí)踐

為了弄清楚這些, 我們有必要了解, Flutter中控件的構(gòu)建流程以及刷新流程. 簡單的做法是在StatelessWidget.build或者StatefulWidget.build中下個(gè)斷點(diǎn), 調(diào)試運(yùn)行, 等斷點(diǎn)停下來. 在Debug視窗的Frames視圖下可以看到函數(shù)的調(diào)用堆棧. 然后繼續(xù)跑幾步, 可以看到后面的調(diào)用流程.

調(diào)用流程

為了更直觀的查看調(diào)用流程, 省去了MaterialAppScaffold的包裹.

例子1
void main() {
  runApp(Sample1());
}

class Sample1 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Text('Sample1', textDirection: TextDirection.ltr,);
  }
}

從這個(gè)調(diào)用棧我們就可以知道調(diào)用流程. 關(guān)鍵函數(shù)我直接提煉出來.

關(guān)鍵代碼分析

關(guān)鍵代碼

  • Widget.canUpdate, 用于判斷Widget是否能復(fù)用, 注意類型相同, key為空也是可以復(fù)用的.

      static bool canUpdate(Widget oldWidget, Widget newWidget) {
      return oldWidget.runtimeType == newWidget.runtimeType
          && oldWidget.key == newWidget.key;
      }
    
  • Element.updateChild

  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ....    
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }
      deactivateChild(child);
      assert(child._parent == null);
    }
    return inflateWidget(newWidget, newSlot);
  }

第一次構(gòu)建時(shí), Element child參數(shù)為空, 我們需要將StatefulWidgetStatelessWidgetbuild出的Widget 傳遞給inflateWidget方法. 后面刷新界面時(shí), Element child參數(shù)不為空時(shí), 我們可以判斷Element原來持有的widget和build新得出的widget是否相同. 什么情況下會這樣了? 當(dāng)我們緩存了Widget就會這樣.

例子2
void main() {
  runApp(Sample2());
}

class _Sample2State extends State<Sample2> {
  int count = 0;
  Text cacheText;

  @override
  void initState() {
    cacheText = Text(
      'cache_text',
      textDirection: TextDirection.ltr,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        RaisedButton(
          child: Text(
            '$count',
            textDirection: TextDirection.ltr,
          ),
          onPressed: () {
            print(this.widget);
            setState(() {
              count += 1;
            });
          },
        ),
        cacheText,
        Text('no_cache_text', textDirection: TextDirection.ltr),
      ],
    );
  }
}

if (child.widget == newWidget)這里下個(gè)斷點(diǎn), 當(dāng)點(diǎn)擊按鈕事刷新時(shí), 我們就可以發(fā)現(xiàn)cache_text會進(jìn)來, 而no_cache_text不會進(jìn)來. no_cache_text會進(jìn)入到if (Widget.canUpdate(child.widget, newWidget))里, 因?yàn)閜arent(Column)沒變, 元素個(gè)數(shù)沒變, 繼續(xù)執(zhí)行update(widget)

  @mustCallSuper
  void update(covariant Widget newWidget) {
    assert(_debugLifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && _active
        && Widget.canUpdate(widget, newWidget));
    _widget = newWidget;
  }

這里其實(shí)就是修改了Element持有的_widget指向. 也就是文檔里說的, Widget被切換, 而Element會被復(fù)用. 當(dāng)這些都無法滿足時(shí), 就是執(zhí)行inflateWidget來創(chuàng)建新的Element. 什么情況下會這樣了, 當(dāng)Widget.canUpdate不為真時(shí), 就是當(dāng)類型或key不同時(shí). 下面舉個(gè)例子:

例子3
class _Sample3State extends State<Sample3> {
  int count = 0;

  GlobalKey keyOne = GlobalKey();
  GlobalKey keyTwo = GlobalKey();

  @override
  void initState() {}

  @override
  Widget build(BuildContext context) {
    List<Widget> list = [
      RaisedButton(
        child: Text(
          '$count',
          textDirection: TextDirection.ltr,
        ),
        onPressed: () {
          print(this.widget);
          setState(() {
            count += 1;
          });
        },
      ),
      Text(
        'key${count%2}',
        textDirection: TextDirection.ltr,
        key: count % 2 == 0 ? keyOne : keyTwo,
      ),
    ];
    if (count % 2 == 0) {
      list.add(RaisedButton(
          onPressed: () {},
          child: Text('button text', textDirection: TextDirection.ltr)));
    } else {
      list.add(Text('just text', textDirection: TextDirection.ltr));
    }
    return Column(
      children: list,
    );
  }
}

Element.inflateWidget下個(gè)斷點(diǎn), 每次點(diǎn)擊按鈕, 都會調(diào)用inflateWidgetColumn的后面2個(gè)Widget. 如果去掉第二個(gè)控件的key屬性, 則不會每次都執(zhí)行Element.inflateWidget. 也就是說一般情況下, 我們無需指定key屬性, Element就能復(fù)用.

文檔里還指出使用了GlobalKey, 一個(gè)Element可以從樹的一個(gè)位置復(fù)用到樹的其它位置. 其相關(guān)核心代碼在inflateWidget_retakeInactiveElement方法里.

  • Element.inflateWidget
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    assert(newWidget != null);
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        assert(newChild._parent == null);
        assert(() { _debugCheckForCycles(newChild); return true; }());
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    assert(() { _debugCheckForCycles(newChild); return true; }());
    newChild.mount(this, newSlot);
    assert(newChild._debugLifecycleState == _ElementLifecycle.active);
    return newChild;
  }

  Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
    final Element element = key._currentElement;
    if (element == null)
      return null;
    if (!Widget.canUpdate(element.widget, newWidget))
      return null;
    assert(() {
      if (debugPrintGlobalKeyedWidgetLifecycle)
        debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this.');
      return true;
    }());
    final Element parent = element._parent;
    if (parent != null) {
      assert(() {
        if (parent == this) {
          throw new FlutterError(
            ...      
          );
        }
        parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
          parent,
          key,
        );
        return true;
      }());
      parent.forgetChild(element);
      parent.deactivateChild(element);
    }
    assert(element._parent == null);
    owner._inactiveElements.remove(element);
    return element;
  }

代碼大概意思是, 首先如果widgetkey值不為空并且為GlobalKey類型時(shí), 會判斷key._currentElement值所指向的widget, 和當(dāng)前widget的類型key都相同. 那么就從舊的父節(jié)點(diǎn)上移除. 作為當(dāng)前的節(jié)點(diǎn)的子widget之一. 否則將進(jìn)行真實(shí)的創(chuàng)建新的Element. 下面舉個(gè)例子

例子4
void main() {
  runApp(Sample4());
}

class Sample4 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _Sample4State();
  }
}

class _Sample4State extends State<Sample4> {
  int count = 0;
  GlobalKey key = GlobalKey();

  @override
  Widget build(BuildContext context) {
    var child;
    if (count % 2 == 0) {
      child = Padding(
        padding: EdgeInsets.all(10.0),
        child: Text(
          'text 2',
          textDirection: TextDirection.ltr,
          key: key,
        ),
      );
    } else {
      child = Container(
        child: Text(
          'text 2',
          textDirection: TextDirection.ltr,
          key: key,
        ),
      );
    }
    return Column(
      children: <Widget>[
        RaisedButton(
          child: Text(
            '$count',
            textDirection: TextDirection.ltr,
          ),
          onPressed: () {
            print(this.widget);
            setState(() {
              count += 1;
            });
          },
        ),
        child
      ],
    );
  }
}

Element._retakeInactiveElement打個(gè)斷點(diǎn), 會發(fā)現(xiàn)Padding里的text 1Container里的text 2復(fù)用了.

GlobalKey

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};

  Element get _currentElement => _registry[this];
  BuildContext get currentContext => _currentElement;
  Widget get currentWidget => _currentElement?.widget;

  T get currentState {
    final Element element = _currentElement;
    if (element is StatefulElement) {
      final StatefulElement statefulElement = element;
      final State state = statefulElement.state;
      if (state is T)
        return state;
    }
    return null;
  }
  ...  
  void GlobalKey._register(Element element) {
    assert(() {
      if (_registry.containsKey(this)) {
        assert(element.widget != null);
        assert(_registry[this].widget != null);
        assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
        _debugIllFatedElements.add(_registry[this]);
      }
      return true;
    }());
    _registry[this] = element;
  }
  ....
}


void Element.mount(Element parent, dynamic newSlot) {
    ...    
    if (widget.key is GlobalKey) {
        final GlobalKey key = widget.key;
        key._register(this);
    }
    ...
}

當(dāng)過閱讀代碼我們可以知道GlobalKeyElement被創(chuàng)建時(shí)就寫入到一個(gè)靜態(tài)Map里, 并且關(guān)聯(lián)了當(dāng)前的Element對象. 所以通過GlobalKey可以查詢當(dāng)前控件相關(guān)的信息. 下面舉個(gè)例子

例子5
void main() {
  runApp(Sample5());
}

class Sample5 extends StatefulWidget {
  @override
  State createState() {
    return _Sample5State();
  }
}

class _Sample5State extends State<Sample5> {
  final key = GlobalKey<Sample5WidgetState>();

  @override
  void initState() {
    //calling the getHeight Function after the Layout is Rendered
    WidgetsBinding.instance.addPostFrameCallback((_) => getHeight());
    super.initState();
  }

  void getHeight() {
    final Sample5WidgetState state = key.currentState;
    final BuildContext context = key.currentContext;
    final RenderBox box = state.context.findRenderObject();

    print(state.number);
    print(box.size.height);
    print(context.size.height);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Sample5Widget(
          key: key,
        ),
      ),
    );
  }
}

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

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

class Sample5WidgetState extends State<Sample5Widget> {
  int number = 12;

  @override
  Widget build(BuildContext context) {
    return new Container(
      child: new Text(
        'text',
        style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
      ),
    );
  }
}

通過將GlobalKey傳遞給下層, 我們在上層通過GlobalKey能夠獲取到Sample5WidgetState對象.

除了上面所述, 那么什么時(shí)候我們需要使用key? 官方有個(gè)例子: Implement Swipe to Dismiss, 為每個(gè)item指定了key屬性'Key'(不是GlobaKey). 也就是對于列表, 為了區(qū)分不同的子項(xiàng), 也可能用到key. 一般key值與當(dāng)前item的數(shù)據(jù)關(guān)聯(lián). 刷新時(shí), 同一個(gè)數(shù)據(jù)指向的item復(fù)用, 不同的則無法復(fù)用.

總結(jié)

  • 對于列表可以使用key唯一關(guān)聯(lián)數(shù)據(jù).
  • GlobaKey可以讓不同的頁面復(fù)用視圖. 參見例子4
  • GlobaKey可以查詢節(jié)點(diǎn)相關(guān)信息. 參見例子5

執(zhí)行流程先復(fù)用widget, 不行就創(chuàng)建widget復(fù)用Element, 再不行就都重新創(chuàng)建.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末劳殖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌途乃,老刑警劉巖溃论,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件况脆,死亡現(xiàn)場離奇詭異那先,居然都是意外死亡部宿,警方通過查閱死者的電腦和手機(jī)栖雾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門楞抡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人析藕,你說我怎么就攤上這事召廷。” “怎么了账胧?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵竞慢,是天一觀的道長。 經(jīng)常有香客問我治泥,道長筹煮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任居夹,我火速辦了婚禮败潦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吮播。我一直安慰自己变屁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布意狠。 她就那樣靜靜地躺著粟关,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闷板,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天澎灸,我揣著相機(jī)與錄音,去河邊找鬼遮晚。 笑死性昭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的县遣。 我是一名探鬼主播糜颠,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼萧求!你這毒婦竟也來了其兴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夸政,失蹤者是張志新(化名)和其女友劉穎元旬,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體守问,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匀归,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耗帕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片穆端。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖兴垦,靈堂內(nèi)的尸體忽然破棺而出徙赢,到底是詐尸還是另有隱情,我是刑警寧澤探越,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布狡赐,位于F島的核電站,受9級特大地震影響钦幔,放射性物質(zhì)發(fā)生泄漏枕屉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一鲤氢、第九天 我趴在偏房一處隱蔽的房頂上張望搀擂。 院中可真熱鬧,春花似錦卷玉、人聲如沸哨颂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽威恼。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箫措,已是汗流浹背腹备。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斤蔓,地道東北人植酥。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像弦牡,于是被迫代替她去往敵國和親友驮。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 本文主要介紹了Flutter布局相關(guān)的內(nèi)容驾锰,對相關(guān)知識點(diǎn)進(jìn)行了梳理喊儡,并從實(shí)際例子觸發(fā),進(jìn)一步講解該如何去進(jìn)行布局稻据。...
    Q吹個(gè)大氣球Q閱讀 9,742評論 6 51
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評論 3 119
  • 上次回來是參加小胡的婚禮买喧,這次是珍珍 和上次相比捻悯,我好像更加冷淡了,更加平靜和看淡了淤毛。 一切都要靠自己今缚,不把自己的...
    Annie周小銀閱讀 176評論 0 0
  • 為什么一個(gè)人靜靜呆著的時(shí)候,流溢出來的都是憂傷低淡? 《憂傷》Erwin Olaf姓言,一組商業(yè)味十足的作品,情緒像死在牛...
    小婦阿達(dá)閱讀 276評論 0 0
  • 在各種痛經(jīng)癥狀中餐塘,比較麻煩的就是“繼發(fā)性痛經(jīng)”,多見于生育皂吮、流產(chǎn)之后或者已經(jīng)人到中年戒傻,以前沒有這種癥狀,但是不知道...
    夏霏兒閱讀 506評論 1 1