說說Flutter中最熟悉的陌生人 —— Key

Key Deer

Key在Flutter的源碼中可以說是無處不在,但是我們?nèi)粘V写_不怎么使用它。有點(diǎn)像是“最熟悉的陌生人”,那么今天就來說說這個“陌生人”,揭開它神秘的面紗。

概念

KeyWidgetElementSemanticsNode的標(biāo)識符。 只有當(dāng)新的WidgetKey與當(dāng)前ElementWidgetKey相同時,它才會被用來更新現(xiàn)有的ElementKey在具有相同父級的Element之間必須是唯一的。

以上定義是源碼中關(guān)于Key的解釋。通俗的說就是Widget的標(biāo)識,幫助實(shí)現(xiàn)Element的復(fù)用。關(guān)于它的說明源碼中也提供了YouTube的視頻鏈接:When to Use Keys。如果你無法訪問,可以看Google 官方在優(yōu)酷上傳的

例子

視頻中的例子很簡單且具有代表性,所以本文將采用它來介紹今天的內(nèi)容。

首先上代碼:

import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> widgets;

  @override
  void initState() {
    super.initState();
    widgets = [
      StatelessColorfulTile(),
      StatelessColorfulTile()
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Row(
        children: widgets,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: _swapTile,
      ),
    );
  }

  _swapTile() {
    setState(() {
      widgets.insert(1, widgets.removeAt(0));
    });
  }
}

class StatelessColorfulTile extends StatelessWidget {

  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  }
}

class Utils {
  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);
  }
}

代碼可以直接復(fù)制到DartPad中運(yùn)行查看效果。 或者點(diǎn)擊這里直接運(yùn)行

效果很簡單,就是兩個彩色方塊,點(diǎn)擊右下角的按鈕后交換兩個方塊的位置。這里我就不放具體的效果圖了。實(shí)際效果也和我們預(yù)期的一樣,兩個方塊成功交換位置。

發(fā)現(xiàn)問題

上面的方塊是StatelessWidget,那我們把它換成StatefulWidget呢?。

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

  @override
  StatefulColorfulTileState createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  }
}

再次執(zhí)行代碼,發(fā)現(xiàn)方塊沒有“交換”。這是為什么?

???

分析問題

首先要知道Flutter中有三棵樹,分別是==Widget Tree==、==Element Tree== 和 ==RenderObject Tree==。

  • Widget: Element配置信息。與Element的關(guān)系可以是一對多,一份配置可以創(chuàng)造多個Element實(shí)例。
  • Element:Widget 的實(shí)例化,內(nèi)部持有WidgetRenderObject
  • RenderObject:負(fù)責(zé)渲染繪制

簡單的比擬一下,Widget有點(diǎn)像是產(chǎn)品經(jīng)理,規(guī)劃產(chǎn)品整理需求。Element則是UI小姐姐,根據(jù)原型整理出最終設(shè)計(jì)圖。RenderObject就是我們程序員,負(fù)責(zé)具體的落地實(shí)現(xiàn)。

代碼中可以確定一點(diǎn),兩個方塊的Widget肯定是交換了。既然Widget沒有問題,那就看看Element

但是為什么StatelessWidget可以成功,換成StatefulWidget就失效了?

點(diǎn)擊按鈕調(diào)用setState方法,依次執(zhí)行:

graph TB
A["_element.markNeedsBuild()"] -- 標(biāo)記自身元素dirty為true --> B["owner.scheduleBuildFor()"]
B --添加至_dirtyElements--> D["drawFrame()"] 
D --> E["buildScope()"]
E --> F["_dirtyElements[index].rebuild()"]
F --> G["performRebuild()"]
G --> H["updateChild()"]

我們重點(diǎn)看一下ElementupdateChild方法:

  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    // 如果'newWidget'為null,而'child'不為null,那么我們刪除'child',返回null。
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      // 兩個widget相同,位置不同更新位置,返回child。這里比較的是hashCode
      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);
        return child;
      }
      deactivateChild(child);
    }
    // 如果無法更新復(fù)用,那么創(chuàng)建一個新的Element并返回。
    return inflateWidget(newWidget, newSlot);
  }

WidgetcanUpdate方法:

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

這里出現(xiàn)了我們今天的主角Key,不過我們先放在一邊。canUpdate方法的作用是判斷newWidget是否可以替代oldWidget作為Element的配置。 一開始也提到了,Element會持有Widget。

該方法判斷的依據(jù)就是runtimeTypekey是否相等。在我們上面的例子中,不管是StatelessWidget還是StatefulWidget的方塊,顯然canUpdate都會返回true。因此執(zhí)行child.update(newWidget)方法,就是將持有的Widget更新了。

不知道這里大家有沒有注意到,這里并沒有更新state。我們看一下StatefulWidget源碼:

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key key }) : super(key: key);
  
  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();
}

StatefulWidget中創(chuàng)建的是StatefulElement,它是Element的子類。

class StatefulElement extends ComponentElement {

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;  
    _state._widget = widget;
  }

  @override
  Widget build() => state.build(this);

  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
  ...
}

通過調(diào)用StatefulWidgetcreateElement方法,最終執(zhí)行createState創(chuàng)建出state并持有。也就是說StatefulElement才持有state。

所以我們上面兩個StatefulWidget的方塊的交換,實(shí)際只是交換了“身體”,而“靈魂”沒有交換。所以不管你怎么點(diǎn)擊按鈕都是沒有變化的。

解決問題

找到了原因,那么怎么解決它?那就是設(shè)置一個不同的Key

  @override
  void initState() {
    super.initState();
    widgets = [
      StatefulColorfulTile(key: const Key("1")),
      StatefulColorfulTile(key: const Key("2"))
    ];
  }

但是這里要注意的是,這里不是說添加key以后,在canUpdate方法返回false,最后執(zhí)行inflateWidget(newWidget, newSlot)方法創(chuàng)建新的Element。(很多相關(guān)文章對于此處的說明都有誤區(qū)。。。好吧我承認(rèn)我一開始也被誤導(dǎo)了。。。)

  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      }
    }
    // 這里就調(diào)用到了createElement,重新創(chuàng)建了Element
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  }

如果如此,那么執(zhí)行createElement方法勢必會重新創(chuàng)建state,那么方塊的顏色也就隨機(jī)變了。當(dāng)然此種情況并不是不存在,比如我們給現(xiàn)有的方塊外包一層PaddingSingleChildRenderObjectElement):

  @override
  void initState() {
    super.initState();
    widgets = [
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("1"),)
      ),
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("2"),)
      ),
    ];
  }

這種情況下,交換后比較外層Padding不變,接著比較內(nèi)層StatefulColorfulTile,因?yàn)閗ey不相同導(dǎo)致顏色隨機(jī)改變。因?yàn)閮蓚€方塊位于不同子樹,兩者在逐層對比中用到的就是canUpdate方法返回false來更改。

而本例是方塊的外層是RowMultiChildRenderObjectElement),是對比兩個List,存在不同。關(guān)鍵在于update時調(diào)用的RenderObjectElement.updateChildren方法。

  @protected
  List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
    ...
    int newChildrenTop = 0;
    int oldChildrenTop = 0;
    int newChildrenBottom = newWidgets.length - 1;
    int oldChildrenBottom = oldChildren.length - 1;

    final List<Element> newChildren = oldChildren.length == newWidgets.length ?
        oldChildren : List<Element>(newWidgets.length);

    Element previousChild;

    // 從前往后依次對比,相同的更新Element,記錄位置,直到不相等時跳出循環(huán)。
    while ((oldChildrenTop <= oldChildrenBottom) && 
        (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      final Widget newWidget = newWidgets[newChildrenTop];
      // 注意這里的canUpdate,本例中在沒有添加key時返回true。
      // 因此直接執(zhí)行updateChild,本循環(huán)結(jié)束返回newChildren。后面因條件不滿足都在不執(zhí)行。
      // 一旦添加key,這里返回false,不同之處就此開始。
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // 從后往前依次對比,記錄位置,直到不相等時跳出循環(huán)。
    while ((oldChildrenTop <= oldChildrenBottom) && 
        (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
      final Widget newWidget = newWidgets[newChildrenBottom];
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      oldChildrenBottom -= 1;
      newChildrenBottom -= 1;
    }
    // 至此,就可以得到新舊List中不同Weiget的范圍。
    final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
    Map<Key, Element> oldKeyedChildren;
    // 如果存在中間范圍,掃描舊children,獲取所有的key與Element保存至oldKeyedChildren。
    if (haveOldChildren) {
      oldKeyedChildren = <Key, Element>{};
      while (oldChildrenTop <= oldChildrenBottom) {
        final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
        if (oldChild != null) {
          if (oldChild.widget.key != null)
            oldKeyedChildren[oldChild.widget.key] = oldChild;
          else
            // 沒有key就移除對應(yīng)的Element
            deactivateChild(oldChild);
        }
        oldChildrenTop += 1;
      }
    }
    // 更新中間不同的部分
    while (newChildrenTop <= newChildrenBottom) {
      Element oldChild;
      final Widget newWidget = newWidgets[newChildrenTop];
      if (haveOldChildren) {
        final Key key = newWidget.key;
        if (key != null) {
          // key不為null,通過key獲取對應(yīng)的舊Element
          oldChild = oldKeyedChildren[key];
          if (oldChild != null) {
            if (Widget.canUpdate(oldChild.widget, newWidget)) {
              oldKeyedChildren.remove(key);
            } else {
              oldChild = null;
            }
          }
        }
      }
      // 本例中這里的oldChild.widget與newWidget hashCode相同,在updateChild中成功被復(fù)用。
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
    }
    
    // 重置
    newChildrenBottom = newWidgets.length - 1;
    oldChildrenBottom = oldChildren.length - 1;

    // 將后面相同的Element更新后添加到newChildren,至此形成新的完整的children。
    while ((oldChildrenTop <= oldChildrenBottom) && 
        (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = oldChildren[oldChildrenTop];
      final Widget newWidget = newWidgets[newChildrenTop];
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // 清除舊列表中多余的Element
    if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
      for (Element oldChild in oldKeyedChildren.values) {
        if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
          deactivateChild(oldChild);
      }
    }

    return newChildren;
  }

這個方法有點(diǎn)復(fù)雜,詳細(xì)的執(zhí)行流程我在代碼中添加了注釋。看完這個diff算法,只能說一句:妙啊!!

到此也就解釋了我們一開始提出的問題。不知道你對這不起眼的key是不是有了更深的認(rèn)識。通過上面的例子可以總結(jié)以下三點(diǎn):

  • 一般情況下不設(shè)置key也會默認(rèn)復(fù)用Element

  • 對于更改同一父級下Widget(尤其是runtimeType不同的Widget)的順序或是增刪,使用key可以更好的復(fù)用Element提升性能

  • StatefulWidget使用key,可以在發(fā)生變化時保持state。不至于發(fā)生本例中“身體交換”的bug。

Key的種類

上面例子中我們用到了Key,其實(shí)它還有許多種類。

在這里插入圖片描述

1.LocalKey

LocalKey 繼承自 Key,在同一父級的Element之間必須是唯一的。(當(dāng)然了,你要是寫成不唯一也行,不過后果自負(fù)哈。。。)

我們基本不直接使用LocalKey ,而是使用的它的子類:

ValueKey

我們上面使用到的Key,其實(shí)就是ValueKey<String>。它主要是使用特定類型的值來做標(biāo)識的,像是“值引用”,比如int、String等類型。我們看它源碼中的 ==操作符方法:

class ValueKey<T> extends LocalKey {
  const ValueKey(this.value);
  
  final T value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ValueKey<T> typedOther = other;
    return value == typedOther.value; // <---
  }
  ...
}

ObjectKey

有“值引用”,就有“對象引用”。主要還是==操作符方法:

class ObjectKey extends LocalKey {
  const ObjectKey(this.value);

  final Object value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ObjectKey typedOther = other;
    return identical(value, typedOther.value); // <---
  }
  ...
}

UniqueKey

會生成一個獨(dú)一無二的key值。

class UniqueKey extends LocalKey {
  UniqueKey();

  @override
  String toString() => '[#${shortHash(this)}]';
}

String shortHash(Object object) {
  return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0');
}

PageStorageKey

用于保存和還原比Widget生命周期更長的值。比如用于保存滾動的偏移量。每次滾動完成時,PageStorage會保存其滾動偏移量。 這樣在重新創(chuàng)建Widget時可以恢復(fù)之前的滾動位置。類似的,在ExpansionTile中用于保存展開與閉合的狀態(tài)。

具體的實(shí)現(xiàn)原理也很簡單,看看PageStorage的源碼就清楚了,這里就不展開了。

2.GlobalKey

介紹

GlobalKey 也繼承自 Key,在整個應(yīng)用程序中必須是唯一的。GlobalKey源碼有點(diǎn)長,我就不全部貼過來了。

@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);

  const GlobalKey.constructor() : super.empty();

  static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
  // 在`Element的 `mount`中注冊GlobalKey。
  void _register(Element element) {
    _registry[this] = element;
  }
  // 在`Element的 `unmount`中注銷GlobalKey。
  void _unregister(Element element) {
    if (_registry[this] == element)
      _registry.remove(this);
  }

  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;
  }
  ...
}

它的內(nèi)部存在一個Map<GlobalKey, Element>的靜態(tài)Map,通過調(diào)用_register_unregister方法來添加和刪除Element。同時它的內(nèi)部還持有當(dāng)前的ElementWidget甚至State。可以看到 GlobalKey是非常昂貴的,沒有特別的復(fù)用需求,不建議使用它

怎么復(fù)用呢?GlobalKey在上面inflateWidget的源碼中出現(xiàn)過一次。當(dāng)發(fā)現(xiàn)key是GlobalKey時,使用_retakeInactiveElement方法復(fù)用Element


  Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
    final Element element = key._currentElement;
    if (element == null)
      return null;
    if (!Widget.canUpdate(element.widget, newWidget))
      return null;
    final Element parent = element._parent;
    if (parent != null) {
      parent.forgetChild(element);
      parent.deactivateChild(element);
    }
    owner._inactiveElements.remove(element);
    return element;
  }

如果獲取到了Element,那么就從舊的節(jié)點(diǎn)上移除并返回。否則將在inflateWidget重新創(chuàng)建新的Element

使用

  • 首先就是上面提到的使用相同的GlobalKey來實(shí)現(xiàn)復(fù)用。

  • 利用GlobalKey持有的BuildContext。比如常見的使用就是獲取Widget的寬高信息,通過BuildContext可以在其中獲取RenderObjectSize,從而拿到寬高信息。這里就不貼代碼了,有需要可以看此處示例

  • 利用GlobalKey持有的State,實(shí)現(xiàn)在外部調(diào)用StatefulWidget內(nèi)部方法。比如常用GlobalKey<NavigatorState>來實(shí)現(xiàn)無Context跳轉(zhuǎn)頁面,在點(diǎn)擊推送信息跳轉(zhuǎn)指定頁面就需要用到。

先創(chuàng)建一個GlobalKey<NavigatorState>

  static GlobalKey<NavigatorState> navigatorKey = new GlobalKey();

添加至MaterialApp:

  MaterialApp(
   navigatorKey: navigatorKey,
   ...
  );

然后就是調(diào)用push方法:

  navigatorKey.currentState.push(MaterialPageRoute(
    builder: (BuildContext context) => MyPage(),
  ));

通過GlobalKey持有的State,就可以調(diào)用其中的方法、獲取數(shù)據(jù)。

LabeledGlobalKey

它是一個帶有標(biāo)簽的GlobalKey。 該標(biāo)簽僅用于調(diào)試,不用于比較。

GlobalObjectKey

同上ObjectKey。區(qū)別在于它是GlobalKey

思考題

最后來個思考題:對于可選參數(shù)key,我搜索了一下Flutter的源碼。發(fā)現(xiàn)只有Dismissible這個滑動刪除組件要求必須傳入key。結(jié)合今天的內(nèi)容,想想是為什么?如果傳入相同的key,會發(fā)生什么?


本篇是“說說”系列第三篇,前兩篇鏈接奉上:

PS:此系列都是自己的學(xué)習(xí)記錄與總結(jié),盡力做到“通俗易懂”和“看著一篇就夠了”。不過也不現(xiàn)實(shí),學(xué)習(xí)之路沒有捷徑。

寫著寫著,就寫的有點(diǎn)多了。本想著拆成兩篇,想想算了。畢竟我是一名月更選手,哈哈~~

如果本文對你有所幫助或啟發(fā)的話,還請不吝點(diǎn)贊收藏支持一波。同時也多多支持我的Flutter開源項(xiàng)目flutter_deer

我們下個月見~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評論 2 372

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

  • 首先,在Flutter中幾乎所有的對象都是一個Widget。跟原生開發(fā)中的“控件”不同,F(xiàn)lutter中的Widg...
    沉江小魚閱讀 1,845評論 0 2
  • 前言 在開發(fā) Flutter 的過程中你可能會發(fā)現(xiàn),一些小部件的構(gòu)造函數(shù)中都有一個可選的參數(shù)——Key。剛接觸的同...
    Vadaski閱讀 6,873評論 15 53
  • 通過實(shí)際案列理解 Flutter 中 Key 在其渲染機(jī)制中起到的作用,從而達(dá)到能在合理的時間和地點(diǎn)使用合理的 K...
    stefanJi閱讀 13,770評論 13 50
  • 前言 前面兩篇Flutter框架分析的文章介紹了渲染流水線,window和框架的初始化。這篇文章繼續(xù)來理一下對Fl...
    HowHardCanItBe閱讀 6,535評論 1 30
  • 3.5.1 初始化列表 C++98中,可以使用"{}"對數(shù)組元素進(jìn)行統(tǒng)一的集合初始值設(shè)定,如 而在C++11中可以...
    zinclee123閱讀 346評論 0 0