Flutter中的Key(一)

本篇主要經過一些實例來深刻分析Flutter中的keyhtml

簡介

經過key.dart中的註釋能夠看到相關說明算法

  • 它是 Widgets, Elements and SemanticsNodes 的標識符
  • 當新Widget的Key和Element相關聯的當前Widget的Key相等時,纔會將Element關聯的Widget更新成最新的Widget
  • 具備相同Parent的Elements,key必須惟一
  • 它有兩個子類 LocalKey和GlobalKey
  • 推薦 www.youtube.com/watch?v=kn0…

Flutter中的內部重建機制,有時候須要配合Key的使用才能觸發真正的「重建」,key一般在widget的構造函數中,當widget在widget樹中移動時,Keys存儲對應的state,在實際中,這將能幫助咱們存儲用戶滑動的位置,修改widget集合等等markdown

什麼是key

大多數時候咱們並不須要key,可是當咱們須要對具備某些狀態且相同類型的組件 進行  添加、移除、或者重排序時,那就須要使用key,不然就會遇到一些古怪的問題,看下例子,
點擊界面上的一個按鈕,而後交換行中的兩個色塊
**less

StatelessWidget 實現

使用 StatelessWidget(StatelessColorfulTile) 作 child(tiles):ide

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)));
 }
}
複製代碼

image.png

StatefulWidget 實現

使用 StatefulWidget(StatefulColorfulTile) 作 child(tiles):函數

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),
       ));
 }
}
複製代碼

結果點擊切換顏色按鈕,沒有反應了
性能

image.png

爲了解決這個問題,咱們在StatefulColorfulTile widget構造時傳入一個UniqueKeyui

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: UniqueKey(),),
    StatefulContainer(key: UniqueKey(),),
  ];
  ···
複製代碼

而後點擊切換按鈕,又能夠愉快地交換顏色了。this

爲何StatelessWidget正常更新,StatefullWidget就更新失效,加了key以後又能夠了呢?爲了弄清楚這其中發生了什麼,咱們須要再次弄清楚Flutter中widget的更新原理
在framework.dart中能夠看到 關於widget的代碼spa

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
  ···
    
  /// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different. 
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
複製代碼

因爲widget只是一個沒法修改的配置,而Element纔是真正被修改使用的對象,在前面的文章能夠知道,當新的widget到來時將會調用canUpdate方法來肯定這個Element是否須要更新,從上面能夠看出,canUpdate 對兩個(新老) Widget 的 runtimeType 和 key 進行比較,從而判斷出當前的** Element 是否須要更新**。

  • StatelessContainer比較

咱們並無傳入key,因此只比較兩個runtimeType,咱們將color定義在widget中,這將使得他們具備不一樣的runtimeType,所以可以更新element 顯示出交換位置的效果

1_sHDIVXBu9RpJYN9Zdn8iBw.gif

  • StatefulContainer比較過程

改爲stateful以後 咱們將color的定義放在在State中,Widget並不保存State,真正hold State的引用是Stateful Element,在咱們沒有給widget設置key以前,將只會比較這兩個widget的runtimeType,因爲兩個widget的屬性和方法都相同,canUpdate方法將返回false,在Flutter看來,沒有發生變化,所以點擊按鈕 色塊並無交換,當咱們給widget一個key之後,canUpdate方法將會比較兩個widget的runtimeType以及key,返回true(這裏runtimeType相同,key不一樣),這樣就能夠正確感知兩個widget交換順序,可是這種比較也是有範圍的tu

如何正確設置key

爲了提高性能,Flutter的diff算法是有範圍的,會對某一個層級的widget進行比較而不是一個個比較,咱們把上面的ok的例子再改動一下,將帶key的 StatefulContainer 包裹上 Padding 組件,而後點擊交換按鈕,會發生下面奇怪的現象。點擊以後不是交換widget,而是從新建立了!

@override
void initState() {
  super.initState();
  tiles = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
  ];
}
複製代碼

對應的widget和Element樹以下

image.png

1_uC-SRZpRkOZCEr_rGisF9g.gif

爲何會出現這種問題,上面提到,Flutter 的  Elemetn to Widget 匹配算法將一次只檢查樹的一個層級:,
(1)顯然Padding並無發生本質的變化
image.png

(2)因而開始第二層的對比,此時發現元素與組件的Key並不匹配,因而把它設置成不可用狀態,可是這裏的key是本地key,(Local Key),Flutter並不能找到另外一層裏面的Key(另一個Padding Widget中的key),所以flutter就建立了一個新的

1_uC-SRZpRkOZCEr_rGisF9g.gif

所以爲了解決這個問題,咱們須要將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(),
    ),
  ];
複製代碼

參考

相關文章
相關標籤/搜索