Flutter - Key

原文在這裏。這篇文章是油管視頻的總結。視頻地址是這裏git

基本上每一個widget都有key參數,可是使用的方法確各有不一樣。在widget從widget樹的一個地方移動到另外一個地方的時候,key會保存狀態。在實際使用中,Key能夠用來保存用戶滾動的位置或者保存集合修改的狀態。github

Key的內部原理

大部分時間用不到Key。加了也不會有什麼反作用,不過也不必消耗額外的空間。就像這樣Map<Foo, Bar> aMap = Map<Foo, Bar>();初始化了一個變量扔着同樣。可是,若是你要對一個同類型,有狀態的widget集合添加、刪除或者排序,那就要Key的的參與了app

爲了說明爲何你修改一個widget集合的時候須要用到key,我(做者)寫了一個簡單的例子。這個例子裏面有兩個widget,隨機顯示顏色。當你點擊裏面的一個按鈕的時候,這兩個組件會互換位置。:less

在無狀態版本里面,有兩個無狀態的組件分別顯示隨機顏色。這個兩個無狀態的widget包含在一個叫作PositionedTiles的有狀態的wiget裏。兩個顯示顏色的widget的位置也保存在裏面。當FloatingActionButton被點擊的時候,兩個無狀態顏色組件就會交換位置。代碼以下:ide

void main() => runApp(new MaterialApp(home: PositionedTiles()));

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

class PositionedTilesState extends State<PositionedTiles> {
 List<Widget> tiles = [
   StatelessColorfulTile(),
   StatelessColorfulTile(),
 ];

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

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

class StatelessColorfulTile extends StatelessWidget {
 Color myColor = UniqueColorGenerator.getColor();
 @override
 Widget build(BuildContext context) {
   return Container(
       color: myColor, child: Padding(padding: EdgeInsets.all(70.0)));
 }
}

可是,若是我讓ColorfulTiles變成有狀態的,顏色都保存在狀態裏,當我點擊按鈕的時候,看起來什麼都不會發生。測試

List<Widget> tiles = [
   StatefulColorfulTile(),
   StatefulColorfulTile(),
];

...
class StatefulColorfulTile extends StatefulWidget {
 @override
 ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
 Color myColor;

 @override
 void initState() {
   super.initState();
   myColor = UniqueColorGenerator.getColor();
 }

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

可是,這個代碼是有bug,點了「交換」按鈕的時候,兩個顏色的widget不會交換。只有在顏色widget裏面加上key參數才能夠達到這個效果。動畫

List<Widget> tiles = [
  StatefulColorfulTile(key: UniqueKey()), // Keys added here
  StatefulColorfulTile(key: UniqueKey()),
];

...
class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);  // NEW CONSTRUCTOR
 
  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

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

可是,只有在修改有狀態的子樹的時候纔是必須的。若是整個子樹的widget集合都是無狀態的,那麼Key並非必須的。ui

這些就是在Flutter裏使用Key所須要知道的所有了。固然,若是你要知道這裏面的原理的話,請繼續往下看。。。spa

爲何Key有的時候是必須的

如你所知,每一個widget都有一個對應的element。就如同構建一個widget樹同樣,Flutter也會構建一個對應的Element樹。這個ElementTree很是簡單,只保存了每一個widget的類型和子element。你能夠認爲element樹是Flutter app的骨架、藍圖。任何其餘的信息均可以從element找到對應的widget而後拿到。3d

在上例的Row widget裏保存了一個有序的子節點列表。當咱們交換Row裏顏色widget的順序的時候,Flutter會遍歷ElementTree,對比交換先後樹的結構是否發生了改變。

Flutter從RowElement開始,而後移動子節點。ElementTree檢查新的widget類型和key與舊的節點是否有不一樣。若是有不一樣,它會把引用指向新的widget。在無狀態版本里,widget並無key,因此Flutter只是檢查了類型。若是這樣看起來信息量太大的話,能夠直接看上面的動圖。

在element樹種,對有狀態widget的處理略微不一樣。仍是會有上文說到的widget和element,不過也會有保存狀態的對象。顏色久保存在這些狀態裏,而不是widget裏。

在有狀態,沒有key的例子裏,當交換widget的按鈕按下的時候,Flutter會遍歷ElementTree,檢查Row的類型,以後更新引用。以後是顏色element,檢查顏色widget是否爲一樣的類型,並更新引用。由於Flutter使用了ElementTree和它的state來決定什麼東西能夠顯示在你的設備上。從用戶的角度看,兩個顏色widget並無正確的互換。

在上面問題的修改版中,顏色widget裏面多了一個key參數。如今再點擊交換顏色的按鈕的時候, Row widget仍是和以前同樣,可是兩個顏色的element的key和widget的key是不一樣的,這樣會致使Flutter在Row element從第一個key值不匹配的地方開始重構element子樹。

以後Flutter會在Row子節點裏找到key值匹配的element來重構子樹。找到一個key值匹配的就更新它對widget的引用。知道整個element子樹重構完成。這樣Flutter就能夠正確的顯示顏色交換了。

言而言之,若是要修改一列狀態widget的數量、順序的時候Key就必不可少了。爲了強調,在本例中顏色的值存在了state裏。state存在有點時候很微小、不起眼,在動畫,用戶輸入數據的顯示和滾動的位置等地方都會用到。

Key放在哪

基本上,若是要在app裏使用key的話,那麼就應該放在存放state的widget子樹的最頂端
一個常常會犯的錯誤是,不少人會把key放在第一個狀態widget裏面,可是這樣是不對的。不信?來稍微修改一下上面的例子。如今Padding widget包在了顏色widget的外面,可是key仍是放在顏色widget上面。

void main() => runApp(new MaterialApp(home: PositionedTiles()));

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

class PositionedTilesState extends State<PositionedTiles> {
  // Stateful tiles now wrapped in padding (a stateless widget) to increase height 
  // of widget tree and show why keys are needed at the Padding level.
  List<Widget> tiles = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
  ];

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

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

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);
 
  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

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

點擊交換按鈕以後,兩個顏色組件顯示出了徹底不一樣的顏色。

這是對應的element樹的樣子:


當咱們交換兩個子widget的位置以後,Flutter裏element到widget的檢查機制每次只會檢查element樹的一層。下圖把葉子節點都灰化處理了,這樣咱們能夠更加註意到底發生了什麼。在Padding widget這一層,全部運做都是正確的。

在第二層,Flutter會發現顏色widget的key和element的key不匹配,它會移除掉這些element的引用。本例中使用的是LocalKeys。也就是說在element的對比中,Flutter只會查看樹的某個範圍內對比key的值是否匹配。

由於在這個範圍內找不到匹配key值的element,那麼它就會建立一個新的,因此會初始化一個新的狀態。因此在本例中,widget顯示的顏色是從新生成的隨機色。


那麼,若是在Padding widget上面加上key值呢?

void main() => runApp(new MaterialApp(home: PositionedTiles()));

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

class PositionedTilesState extends State<PositionedTiles> {
  List<Widget> tiles = [
    Padding(
      // Place the keys at the *top* of the tree of the items in the collection.
      key: UniqueKey(), 
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(),
    ),
  ];

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

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

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);
 
  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

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

Flutter注意到了問題,並會正確的更新。

我應該用哪一種Key

咱們要用的key的類型主要看widget要作到什麼特色的區分。下面要介紹四種key:ValueKey, ObjectKey, UniqueKeyPageStorageKey, GlobalKey.

ValueKey

好比下面的todo app,你能夠對各條目從新排序。

在這個場景下,若是一個條目的文本能夠認爲是一個常量,而且是惟一的,那麼就是用於ValueKey。文本就是「value」值。

return TodoItem(
  key: ValueKey(todo.task),
  todo: todo,
  onDismissed: (direction) => _removeTodo(context, todo),
);

Objectkey

另外的一個場景,好比你有一個地址簿app。裏面保存了不一樣人的信息。在這個狀況下,每一個widget都保存了一個複雜的數據。每一個單獨的字段,好比姓名或者出生日期均可能和其餘的數據是同樣,可是這些數據組合起來就是惟一的。那麼,這就很實用於ObjectKey

Uniquekey

若是多個widget有一樣的值,或者你想要確保每一個widget都不一樣,那麼就可使用UniqueKey。上面的例子中就使用了UniqueKey,由於在顏色widget裏面並無其餘的值能夠區分於其餘的widget了。使用UniqueKey要當心。若是你在build方法裏建立了一個新的UniqueKey,那麼這個widget每次調用build方法以後都會獲得一個不一樣的UniqueKey這樣就把key的好處所有的抹煞了。

相似的,千萬不要考慮使用隨機數來做爲你的key。每次一個widget調用了build方法就會生成一個隨機數,那麼多個幀的連續性也就被破壞了。那麼,效果也就和一開始就沒用key的效果是同樣的了。

PageStoragekey

這是一個很特殊的key,它保存了用戶滾動的位置,這樣app能夠保存用戶滾動的位置給下次用戶打開的時候直接到上次滾動的位置。

GlobalKey

有兩個用處:

  • 能夠在app的任何地方更換父widget而不會丟失狀態
  • 它能夠用來從徹底不一樣的widget樹裏面訪問數據

第一種狀況的一個例子是若是你要在不一樣的地方顯示同一個widget,並且state也是相同的,那麼GlobalKey就是最好的選擇。

第二種狀況,若是你想要驗證一個密碼,可是又不想在不一樣的widget之間共享狀態。

GlobalKey也能夠用於測試,使用一個key來訪問某個特定的widget,而後查看裏面的數據。

一般(並非所有),GlobalKey更像是一個全局變量。老是有其餘的方法能夠訪問state,好比InheritedWidget,或者相似Redux的庫或者BLoC模式的實現。

總結

總而言之,在widget樹裏面保存state就要考慮用key了。通常要修改一列一樣類型的widget的時候,好比一個列表。把key值放在要保存state的子樹的頂層widget上。根據要展示的數據和使用的場景選擇合適的key類型。

Todo app的代碼在這裏

相關文章
相關標籤/搜索