詳解Flutter中key的正確使用方式
1、什么是key
Widget中有個(gè)可選屬性key,顧名思義,它是組件的標(biāo)識(shí)符,當(dāng)設(shè)置了key,組件更新時(shí)會(huì)根據(jù)新老組件的key是否相等來進(jìn)行更新,可以提高更新效率。但一般我們不會(huì)去設(shè)置它,除非對(duì)某些具備狀態(tài)且相同的組件進(jìn)行添加、移除、或者排序時(shí),就需要使用到key,不然就會(huì)出現(xiàn)一些莫名奇妙的問題。
例如下面的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), ); } }
點(diǎn)擊刪除按鈕,從ColorBlock的列表中刪除第一個(gè)元素,可以觀察到顏色發(fā)生了錯(cuò)亂,刪除了1號(hào)色塊,它的顏色狀態(tài)轉(zhuǎn)移到了2號(hào)身上。這種情況在實(shí)際開發(fā)中往往會(huì)造成不小的麻煩。
這時(shí),就需要為每個(gè)ColorBlock設(shè)置key值,來避免這個(gè)問題。
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'), ];
點(diǎn)擊刪除按鈕,可以看到顏色錯(cuò)亂的現(xiàn)象消失了,一切正常。那么有沒有想過,為什么ColorBlock有key和沒key會(huì)出現(xiàn)這種差異?
2、key的更新原理
我們來簡(jiǎn)單分析下key的更新原理。
首先,我們知道Widget是組件配置信息的描述,而Element才是Widget的真正實(shí)現(xiàn),負(fù)責(zé)組件的布局和渲染工作。在創(chuàng)建Widget時(shí)會(huì)對(duì)應(yīng)的創(chuàng)建Element,Element保存著Widget的信息。
當(dāng)我們更新組件時(shí)(通常指調(diào)用setState方法)會(huì)遍歷組件樹,對(duì)組件進(jìn)行新舊配置的對(duì)比,如果同個(gè)組件信息不一致,則進(jìn)行更新操作,反之則不作任何操作。
/// 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)) { /// 判斷新舊組件為同一個(gè)組件則進(jìn)行更新操作 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 { /// 創(chuàng)建邏輯走這里 newChild = inflateWidget(newWidget, newSlot); } return newChild; }
通過Element中的updateChild進(jìn)行組件的更新操作,其中Widget.canUpdate是判斷組件是否需要更新的核心。
/// Widget static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
canUpdate的代碼很簡(jiǎn)單,就是對(duì)比新老組件的runtimeType和key是否一致,一致剛表示為同一個(gè)組件需要更新。
結(jié)合demo,當(dāng)刪除操作時(shí),列表中第一個(gè)的組件oldWidget為ColorBlock(text: '1'),newWidget為ColorBlock(text: '2') ,因?yàn)槲覀儗ext和color屬性都存儲(chǔ)在State中,所以 oldWidget.runtimeType == newWidget.runtimeType為true,oldWidget.key == newWidget.key 為null,也等于true。
于是調(diào)用udpate進(jìn)行更新
/// Element void update(covariant Widget newWidget) { _widget = newWidget; }
可以看出,update也只是簡(jiǎn)單的更新Element對(duì)Widget的引用。 最終新的widget更新為ColorBlock(text: '2'),State依舊是ColorBlock(text: '1')的State,內(nèi)部的狀態(tài)保持不變。
如果添加了Key,剛oldWidget.key == newWidget.key為false,不會(huì)走update流程,也就不存在這個(gè)問題。
3、key的分類
key有兩個(gè)子類GlobalKey和LocalKey。
GlobalKey
GlobalKey全局唯一key,每次build的時(shí)候都不會(huì)重建,可以長(zhǎng)期保持組件的狀態(tài),一般用來進(jìn)行跨組件訪問Widget的狀態(tài)。
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的內(nèi)部 (_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設(shè)置為GlobalKey,可以通過實(shí)例訪問組件的內(nèi)部屬性和方法。達(dá)到跨組件操作的目的。
LocalKey
LocalKey局部key,可以保持當(dāng)前組件內(nèi)的子組件狀態(tài),用法跟GlobalKey類似,可以訪問組件內(nèi)部的數(shù)據(jù)。
LocalKey有3個(gè)子類ValueKey、ObjectKey、UniqueKey。
- ValueKey
可以使用任何值做為key,比較的是兩個(gè)值之間是否相等于。
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對(duì)象作為Key,比較的是兩個(gè)對(duì)象內(nèi)存地址是否相同,也就是說兩個(gè)對(duì)象是否來自同一個(gè)類的引用。
class ObjectKey extends LocalKey { const ObjectKey(this.value); final Object? value; @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; /// identical函數(shù): 檢查兩個(gè)引用是否指向同一對(duì)象 return other is ObjectKey && identical(other.value, value); } /// ... }
- UniqueKey
獨(dú)一無二的key,Key的唯一性,一旦使用UniqueKey,那么將不存在element復(fù)用
class UniqueKey extends LocalKey { UniqueKey(); @override String toString() => '[#${shortHash(this)}]'; }
總結(jié)
1、key是Widget中的唯一標(biāo)識(shí),如果列表中包含有狀態(tài)組件,對(duì)其進(jìn)行添加、移除、或者排序操作,必須增加key。以避免出現(xiàn)亂序現(xiàn)象。
2、出現(xiàn)亂序現(xiàn)象的根本原因是:新舊組件通過runtimeType和key進(jìn)行對(duì)比,key為空的情況下,有狀態(tài)組件runtimeType對(duì)比為true,造成組件更新后依然保持State內(nèi)部的屬性狀態(tài)。
3、key分為GlobalKey和LocalKey,GlobalKey可以進(jìn)行跨組件訪問Widget,LocalKey只能在同級(jí)之下進(jìn)行。
以上就是詳解Flutter中key的正確使用方式的詳細(xì)內(nèi)容,更多關(guān)于Flutter key使用方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法
這篇文章主要介紹了Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06詳解Android中Activity的四大啟動(dòng)模式實(shí)驗(yàn)簡(jiǎn)述
本篇文章主要介紹了Android中Activity的四大啟動(dòng)模式實(shí)驗(yàn)簡(jiǎn)述,具有一定的參考價(jià)值,有興趣的可以了解一下。2016-12-12Android使用AlertDialog實(shí)現(xiàn)對(duì)話框
本文主要介紹了Android使用AlertDialog實(shí)現(xiàn)對(duì)話框的相關(guān)知識(shí),具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-03-03Android編程實(shí)現(xiàn)ListView滾動(dòng)提示等待框功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)ListView滾動(dòng)提示等待框功能,結(jié)合實(shí)例形式分析了Android ListView滾動(dòng)事件相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-02-02Android和JavaScript相互調(diào)用的方法
這篇文章主要介紹了Android和JavaScript相互調(diào)用的方法,實(shí)例分析了Android的WebView執(zhí)行JavaScript及JavaScript訪問Android的技巧,需要的朋友可以參考下2015-12-12Android獲取WebView加載url的請(qǐng)求錯(cuò)誤碼 【推薦】
這篇文章主要介紹了Android獲取WebView加載url的請(qǐng)求錯(cuò)誤碼 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06Android EventBus 3.0.0 使用總結(jié)(必看篇)
下面小編就為大家?guī)硪黄狝ndroid EventBus 3.0.0 使用總結(jié)(必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05