首頁 > 軟體

詳解Flutter中key的正確使用方式

2023-01-20 14:02:12

1、什麼是key

Widget中有個可選屬性key,顧名思義,它是元件的識別符號,當設定了key,元件更新時會根據新老元件的key是否相等來進行更新,可以提高更新效率。但一般我們不會去設定它,除非對某些具備狀態且相同的元件進行新增、移除、或者排序時,就需要使用到key,不然就會出現一些莫名奇妙的問題。

例如下面的demo:

import 'dart:math';
import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'test',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('key demo'),
        ),
        body: const KeyDemo(),
      ),
    );
  }
}
class KeyDemo extends StatefulWidget {
  const KeyDemo({Key? key}) : super(key: key);
  @override
  State<StatefulWidget> createState() => _KeyDemo();
}
class _KeyDemo extends State<KeyDemo> {
  final List<ColorBlock> _list = [
    const ColorBlock(text: '1'),
    const ColorBlock(text: '2'),
    const ColorBlock(text: '3'),
    const ColorBlock(text: '4'),
    const ColorBlock(text: '5'),
  ];
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ..._list,
        ElevatedButton(
          onPressed: () {
            _list.removeAt(0);
            setState(() {});
          },
          child: const Text('刪除'),
        )
      ],
    );
  }
}
class ColorBlock extends StatefulWidget {
  final String text;
  const ColorBlock({Key? key, required this.text}) : super(key: key);
  @override
  State<StatefulWidget> createState() => _ColorBlock();
}
class _ColorBlock extends State<ColorBlock> {
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: 50,
      color: color,
      child: Text(widget.text),
    );
  }
}

點選刪除按鈕,從ColorBlock的列表中刪除第一個元素,可以觀察到顏色發生了錯亂,刪除了1號色塊,它的顏色狀態轉移到了2號身上。這種情況在實際開發中往往會造成不小的麻煩。

這時,就需要為每個ColorBlock設定key值,來避免這個問題。

final List<ColorBlock> _list = [
    const ColorBlock(key: ValueKey('1'), text: '1'),
    const ColorBlock(key: ValueKey('2'), text: '2'),
    const ColorBlock(key: ValueKey('3'), text: '3'),
    const ColorBlock(key: ValueKey('4'), text: '4'),
    const ColorBlock(key: ValueKey('5'), text: '5'),
  ];

點選刪除按鈕,可以看到顏色錯亂的現象消失了,一切正常。那麼有沒有想過,為什麼ColorBlock有key和沒key會出現這種差異?

2、key的更新原理

我們來簡單分析下key的更新原理。

首先,我們知道Widget是元件設定資訊的描述,而Element才是Widget的真正實現,負責元件的佈局和渲染工作。在建立Widget時會對應的建立Element,Element儲存著Widget的資訊。

當我們更新元件時(通常指呼叫setState方法)會遍歷元件樹,對元件進行新舊設定的對比,如果同個元件資訊不一致,則進行更新操作,反之則不作任何操作。

/// Element
 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    /// 更新邏輯走這裡
    if (child != null) {
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {  
        /// 判斷新舊元件為同一個元件則進行更新操作 
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
        if (!kReleaseMode && debugProfileBuildsEnabled)
          Timeline.finishSync();
      }
    } else {
      /// 建立邏輯走這裡
      newChild = inflateWidget(newWidget, newSlot);
    }
    return newChild;
  }

通過Element中的updateChild進行元件的更新操作,其中Widget.canUpdate是判斷元件是否需要更新的核心。

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

canUpdate的程式碼很簡單,就是對比新老元件的runtimeType和key是否一致,一致剛表示為同一個元件需要更新。

結合demo,當刪除操作時,列表中第一個的元件oldWidget為ColorBlock(text: '1'),newWidget為ColorBlock(text: '2') ,因為我們將text和color屬性都儲存在State中,所以 oldWidget.runtimeType == newWidget.runtimeType為true,oldWidget.key == newWidget.key 為null,也等於true。

於是呼叫udpate進行更新

/// Element
void update(covariant Widget newWidget) {
    _widget = newWidget;
}

可以看出,update也只是簡單的更新Element對Widget的參照。 最終新的widget更新為ColorBlock(text: '2'),State依舊是ColorBlock(text: '1')的State,內部的狀態保持不變。

如果新增了Key,剛oldWidget.key == newWidget.key為false,不會走update流程,也就不存在這個問題。

3、key的分類

key有兩個子類GlobalKey和LocalKey。

GlobalKey

GlobalKey全域性唯一key,每次build的時候都不會重建,可以長期保持元件的狀態,一般用來進行跨元件存取Widget的狀態。

class GlobalKeyDemo extends StatefulWidget {
  const GlobalKeyDemo({Key? key}) : super(key: key);
  @override
  State<StatefulWidget> createState() => _GlobalKeyDemo();
}
class _GlobalKeyDemo extends State<GlobalKeyDemo> {
  GlobalKey _globalKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ColorBlock(
          key: _globalKey,
        ),
        ElevatedButton(
          onPressed: () {
            /// 通過GlobalKey可以存取元件ColorBlock的內部
            (_globalKey.currentState as _ColorBlock).setColor();
            setState(() {});
          },
          child: const Text('更新為紅色'),
        )
      ],
    );
  }
}
class ColorBlock extends StatefulWidget {
  const ColorBlock({Key? key}) : super(key: key);
  @override
  State<StatefulWidget> createState() => _ColorBlock();
}
class _ColorBlock extends State<ColorBlock> {
  Color color = Colors.blue;
  setColor() {
    color = Colors.red;
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: 50,
      color: color,
    );
  }
}

將元件的key設定為GlobalKey,可以通過範例存取元件的內部屬性和方法。達到跨元件操作的目的。

LocalKey

LocalKey區域性key,可以保持當前元件內的子元件狀態,用法跟GlobalKey類似,可以存取元件內部的資料。

LocalKey有3個子類ValueKey、ObjectKey、UniqueKey。

  • ValueKey

可以使用任何值做為key,比較的是兩個值之間是否相等於。

class ValueKey<T> extends LocalKey {
 const ValueKey(this.value);
 final T value;
 @override
 bool operator ==(Object other) {
   if (other.runtimeType != runtimeType)
     return false;
   return other is ValueKey<T>
       && other.value == value;
 }
/// ...
}
  • ObjectKey:

可以使用Object物件作為Key,比較的是兩個物件記憶體地址是否相同,也就是說兩個物件是否來自同一個類的參照。

class ObjectKey extends LocalKey {
  const ObjectKey(this.value);
  final Object? value;
  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    /// identical函數: 檢查兩個參照是否指向同一物件
    return other is ObjectKey
        && identical(other.value, value);
  }
  /// ... 
}
  • UniqueKey

獨一無二的key,Key的唯一性,一旦使用UniqueKey,那麼將不存在element複用

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

總結

1、key是Widget中的唯一標識,如果列表中包含有狀態元件,對其進行新增、移除、或者排序操作,必須增加key。以避免出現亂序現象。

2、出現亂序現象的根本原因是:新舊元件通過runtimeType和key進行對比,key為空的情況下,有狀態元件runtimeType對比為true,造成元件更新後依然保持State內部的屬性狀態。

3、key分為GlobalKey和LocalKey,GlobalKey可以進行跨元件存取Widget,LocalKey只能在同級之下進行。

以上就是詳解Flutter中key的正確使用方式的詳細內容,更多關於Flutter key使用方式的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com