Widget
在Flutter中,一切皆是Widget(組件),Widget的功能是“描述一個UI元素的配置數(shù)據(jù)”,它就是說,Widget其實并不是表示最終繪制在設(shè)備屏幕上的顯示元素,它只是描述顯示元素的一個配置數(shù)據(jù)。
實際上,F(xiàn)lutter中真正代表屏幕上顯示元素的類是 Element,也就是說Widget 只是描述 Element 的配置數(shù)據(jù)。并且一個 Widget 可以對應(yīng)多個 Element,因為同一個 Widget 對象可以被添加到 UI樹的不同部分,而真正渲染時,UI樹的每一個 Element 節(jié)點都會對應(yīng)一個 Widget 對象。
兩種Widget模型
StatelessWidget
StatelessWidget用于不需要維護狀態(tài)的場景,其對應(yīng)的Element是StatelessElement
StatefulWidget
相反,StatefulWidget用于需要維護狀態(tài)的場景,其對應(yīng)的Element是StatefulElement,StatefulElement持有State
createState() 用于創(chuàng)建和StatefulWidget相關(guān)的狀態(tài),它在StatefulWidget的生命周期中可能會被多次調(diào)用。
例如,當一個StatefulWidget同時插入到widget樹的多個位置時,F(xiàn)lutter framework就會調(diào)用該方法為每一個位置生成一個獨立的State實例,其實,本質(zhì)上就是一個StatefulElement對應(yīng)一個State實例。
生命周期
理論
Flutter 中說的生命周期,是獨指有狀態(tài)組件(StatefulWidget)的生命周期,對于無狀態(tài)組件生命周期只有一次 build 這個過程,也只會渲染一次,StatefulWidget生命周期圖如下:
Flutter 中的生命周期,包含以下幾個階段:
- createState :該函數(shù)為 StatefulWidget 中創(chuàng)建 State 的方法,當 StatefulWidget 被調(diào)用時會立即執(zhí)行 createState 。
- initState :該函數(shù)為 State 初始化調(diào)用,緊接著createState之后調(diào)用,可以在此期間執(zhí)行 State 各變量的初始賦值,同時也可以在此期間與服務(wù)端交互
- didChangeDependencies :第一種情況是StatefulElement mount時會回調(diào),這種情況會緊跟initState被回調(diào)
還有一種情況是當State對象的“依賴”發(fā)生變化時會被調(diào)用,這種依賴是指通過context.dependOnInheritedWidgetOfExactType進行的依賴 - build :主要是返回需要渲染的 Widget ,由于 build 會被調(diào)用多次,因此在該函數(shù)中只能做返回 Widget 相關(guān)邏輯
- reassemble, 在 debug 模式下,每次熱重載都會調(diào)用該函數(shù),因此在 debug 階段可以在此期間增加一些 debug 代碼,來檢查代碼問題。
- didUpdateWidget ,在widget重新構(gòu)建時,F(xiàn)lutter framework會調(diào)用Widget.canUpdate來檢測Widget樹中同一位置的新舊節(jié)點,然后決定是否需要更新,如果Widget.canUpdate返回true則會調(diào)用此回調(diào)。Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被調(diào)用。父組件發(fā)生 build 的情況下,子組件該方法才會被調(diào)用,其次該方法調(diào)用之后一定會再調(diào)用本組件中的 build 方法。
- deactivate ,在組件被移除節(jié)點后會被調(diào)用,如果該組件被移除節(jié)點,然后未被插入到其他節(jié)點時,則會繼續(xù)調(diào)用 dispose 永久移除。
- dispose ,永久移除組件,并釋放組件資源。
Flutter 生命周期的整個過程可以分為四個階段
- 初始化階段:createState 和 initState
- 組件創(chuàng)建階段:didChangeDependencies didUpdateWidget 和 build
- 組件銷毀階段:deactivate 和 dispose
實例
class LifeCycleTest extends StatefulWidget {
final String TAG = "LifeCycleTest";
State<StatefulWidget> createState() {
print('$TAG createState');
return LifeCycleTestState();
}
}
class LifeCycleTestState extends State<LifeCycleTest> {
void initState() {
print('${widget.TAG} initState');
super.initState();
}
void reassemble() {
print('${widget.TAG} reassemble');
super.reassemble();
}
void didChangeDependencies() {
print('${widget.TAG} didChangeDependencies');
super.didChangeDependencies();
}
void didUpdateWidget(covariant LifeCycleTest oldWidget) {
print('${widget.TAG} didUpdateWidget');
super.didUpdateWidget(oldWidget);
}
void deactivate() {
print('${widget.TAG} deactivate');
super.deactivate();
}
void dispose() {
print('${widget.TAG} dispose');
super.dispose();
}
Widget build(BuildContext context) {
print('${widget.TAG} build');
return Scaffold(
body: SafeArea(
child: Column(
children: [
GestureDetector(
onTap: () {
setState(() {});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
MyInheritedWidget(LifeCycleTestChild(), 20)
],
),
),
);
}
}
class LifeCycleTestChild extends StatefulWidget {
final String TAG = "LifeCycleTestChild";
State<StatefulWidget> createState() {
print('$TAG createState');
return LifeCycleTestChildState();
}
}
class LifeCycleTestChildState extends State<LifeCycleTestChild> {
void initState() {
print('${widget.TAG} initState');
super.initState();
}
void reassemble() {
print('${widget.TAG} reassemble');
super.reassemble();
}
void didChangeDependencies() {
print('${widget.TAG} didChangeDependencies');
super.didChangeDependencies();
}
void didUpdateWidget(covariant LifeCycleTestChild oldWidget) {
print('${widget.TAG} didUpdateWidget');
super.didUpdateWidget(oldWidget);
}
void deactivate() {
print('${widget.TAG} deactivate');
super.deactivate();
}
void dispose() {
print('${widget.TAG} dispose');
super.dispose();
}
Widget build(BuildContext context) {
print('${widget.TAG} build');
var dependOnInheritedWidgetOfExactType =
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
return Container(
child: Text("${(dependOnInheritedWidgetOfExactType as MyInheritedWidget).count}"),
);
}
}
class MyInheritedWidget extends InheritedWidget {
final int count;
MyInheritedWidget(
Widget child,
this.count,
) : super(child: child);
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return true;
}
}
上面圖說明了幾個點
- 第二個圖:didChangeDependencies在widget第一次初始化的時候都會調(diào)用
- 第二個圖:LifeCycleTest組件發(fā)生build,LifeCycleTestChild子組件調(diào)用didUpdateWidget,自身并沒有調(diào)用didUpdateWidget,第三個圖,熱加載后都調(diào)用了didUpdateWidget,說明了父組件發(fā)生 build 的情況下,子組件該方法才會被調(diào)用
- 第三個圖:熱加載后,只有LifeCycleTestChild調(diào)用了didChangeDependencies,說明通過context.dependOnInheritedWidgetOfExactType進行的依賴會調(diào)用該方法
Getx生命周期
先看下Controller的集成層級
再看下對應(yīng)類的定義
其中 GetxController只是有個 update 方法用于通知組件刷新。
在 DisposableInterface 中覆蓋了onInit 方法,實際多干了一件事,就是監(jiān)聽第一幀回調(diào),等第一幀回調(diào)過來之后再調(diào)用onReady
然后我們再看下這些生命周期分別是在什么時候調(diào)用的
- onInit:組件在內(nèi)存分配后會被馬上調(diào)用,可以在這個方法對 controller 做一些初始化工作。
- onReady:在 onInit 一幀后被調(diào)用,適合做一些導航進入的事件,例如對話框提示、SnackBar 或異步網(wǎng)絡(luò)請求。
- onClose:在 onDelete 方法前調(diào)用、用于銷毀 controller 使用的資源,例如關(guān)閉事件監(jiān)聽,關(guān)閉流對象,或者銷毀可能造成內(nèi)存泄露的對象,例如 TextEditingController,AniamtionController。也適用于將數(shù)據(jù)進行離線持久化。
所以有了 GetxController 的生命周期后,我們就可以完全替換掉 StatefulWidget 了。
onInit 或 onReady替換 initState
onClose 替換 dispose,比如關(guān)閉流
Key
key的作用是:控制Element樹上的Element是否被復用
如果兩個widget的runtimeType和key相等(用==比較),那么原本指向舊widge的element,它的指針會指向新的widget上(通過Element.update方法)。如果不相等,那么舊element會從樹上移除,根據(jù)當前新的widget重新構(gòu)建新element,并加到樹上指向新widget。
基于Element的復用機制的解釋
在Flutter中,Widget是不可變的,它僅僅作為配置信息的載體而存在,并且任何配置或者狀態(tài)的更改都會導致Widget的銷毀和重建,但好在Widget本身是非常輕量級的,因此實際耗費的性能很小。與之相反,RenderObject就不一樣了,實例化一個RenderObject的成本是非常高的,頻繁地實例化和銷毀RenderObject對性能的影響非常大,因此為了高性能地構(gòu)建用戶界面,F(xiàn)lutter使用Element的復用機制來盡可能地減少RenderObject的頻繁創(chuàng)建和銷毀。當Widget改變的時候,Element會通過組件類型以及對應(yīng)的Key來判斷舊的Widget和新的Widget是否一致:
1、如果某一個位置的舊Widget和新Widget不一致,就會重新創(chuàng)建Element,重建Element的同時也重建了RenderObject;
2、如果某一個位置的舊Widget和新Widget一致,只是配置發(fā)生了變化,比如組件的顏色變了,此時Element就會被復用,而只需要修改Widget對應(yīng)的Element的RenderObject中的顏色設(shè)置即可,無需再進行十分耗性能的RenderObject的重建工作。
分類
flutter 中的key總的來說分為以下兩種:
- 局部鍵(LocalKey):ValueKey、ObjectKey、UniqueKey
- 全局鍵(GlobalKey):GlobalObjectKey
ValueKey
ValueKey是通過某個具體的Value值來做區(qū)分的Key,如下:
key:ValueKey(1),
key:ValueKey("2"),
key:ValueKey(true),
key:ValueKey(0.1),
key:ValueKey(Person()), // 自定義類實例
可以看到,ValueKey的值可以是任意類型,甚至可以是我們自定義的類的實例。判斷2個ValueKey是否相等是根據(jù)里面的value是否來判斷的,如果value是自定義類,則可以通過重寫自定義類的操作符來實現(xiàn)
例如,現(xiàn)在有一個展示所有學生信息的ListView列表,每一項itemWidget所對應(yīng)的學生對象均包含某個唯一的屬性,例如學號、身份證號等,那么這個時候就可以使用ValueKey,其值就是對應(yīng)的學號或者身份證號。
ObjectKey
ObjectKey的使用場景如下:
現(xiàn)有一個所有學生信息的ListView列表,每一項itemWidget對應(yīng)的學生對象不存在某個唯一的屬性(比如學號、身份證號),任一屬性均有可能與另外一名學生重復,只有多個屬性組合起來才能唯一的定位到某個學生,那么此時使用ObjectKey就最合適不過了。
ObjectKey判斷兩個Key是否相同的依據(jù)是:兩個對象是否具有相同的內(nèi)存地址,不論自定義對象是否重寫了==運算符判斷,均會被視為不同的Key
UniqueKey
顧名思義,UniqueKey是一個唯一鍵,不需要參數(shù),并且每一次刷新都會生成一個新的Key。
一旦使用UniqueKey那么就不存在Element復用了
GlobalKey
GlobalKey是全局唯一的鍵,一般而言,GlobalKey有如下幾種用途:
- 獲取配置、狀態(tài)以及組件Element
- _globalKey.currentWidget:獲取當前組件的配置信息(存在widget樹中)
- _globalKey.currentState:獲取當前組件的狀態(tài)信息(存在Element樹中)
- _globalKey.currentContext:獲取當前組件的Element
- 實現(xiàn)組件的局部刷新
將需要單獨刷新的widget從復雜的布局中抽離出去,然后通過傳GlobalKey引用,這樣就可以通過GlobalKey實現(xiàn)跨組件的刷新了。
key作用示例
一般情況下我們不使用key,程序也是能正常運行的,只有部分特殊情況下需要使用key,下面我們看一個例子
import 'dart:math';
import 'package:flutter/material.dart';
class PositionedTiles extends StatefulWidget {
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
late List<Widget> tiles;
void initState() {
super.initState();
tiles = [
// StatefulColorfulTile(),
// StatefulColorfulTile(),
// StatefulColorfulTile(key: UniqueKey()),
// StatefulColorfulTile(key: UniqueKey()),
StatelessColorfulTile(),
StatelessColorfulTile(),
];
}
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: tiles,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied),
// child: Icon(Icons.sentiment_very_dissatisfied),
onPressed: swapTiles,
),
);
}
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
// ignore: must_be_immutable
class StatelessColorfulTile extends StatelessWidget {
Color color = ColorUtil.randomColor(); //color屬性直接在widget中
Widget build(BuildContext context) {
return Container(color: color, child: Padding(padding: EdgeInsets.all(70.0)));
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key? key}) : super(key: key);
State<StatefulWidget> createState() => StatefulColorfulTileState();
}
class StatefulColorfulTileState extends State<StatefulColorfulTile> {
Color? color; //color屬性在State中
void initState() {
super.initState();
color = ColorUtil.randomColor();
print('initState');
}
Widget build(BuildContext context) {
return Container(color: color, child: Padding(padding: EdgeInsets.all(70.0)));
}
}
class ColorUtil {
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);
}
}
上面的代碼效果如下,可以看到使用StatelessColorfulTile時,點擊按鈕后兩個色塊能成功交換:
當我們把代碼換成下面這樣
神奇的事情發(fā)生了,點擊交換按鈕沒有任何反應(yīng)
那在使用StatefulColorfulTile的前提下,如何讓色塊再次點擊按鈕后能發(fā)生交換呢?我猜聰明的你已經(jīng)想到了,就是設(shè)置key屬性,即把代碼改成下面這個樣子
下面我們來解釋下為什么會出現(xiàn)這樣的結(jié)果:
-
為什么StatelessWidget的能交換
當代碼調(diào)用PositionedTiles.setState交換兩個Widget后,flutter會從上到下逐一對比Widget樹和Element樹中的每個節(jié)點,如果發(fā)現(xiàn)節(jié)點的runtimeType和key一致的話(這里沒有key,因此只對比runtimeType),那么就認為該Element仍然是有效的,可用復用,于是只需要更改Element的指針,就可以直接復用
對于StatelessWidget中的color信息是直接在widget中的,那widget重新build直接就更新了顏色 -
為啥StatefulColorfulTile要加key才能交換
StatefulWidget的color屬性是放在State中的,我們上面說過State被Element管理
我們先看下不帶key時的樹結(jié)構(gòu)
首先還是Widget更新后,flutter會根據(jù)runtimeType和key比較Widget從而判斷是否需要重新構(gòu)建Element,這里key為空,只比較runtimeType,比較結(jié)果必然相等,所以Element直接復用。
StatefulColorfulTile在重新渲染時,Color屬性不再是從Widget對象(即自身)里獲取,而是從Element的State里面獲取,而Element根本沒發(fā)生變化,所以取到的Color也沒有變化,最終就算怎么渲染,顏色都是不變的,視覺效果上也就是兩個色塊沒有交換了。
接著看有了key之后的樹結(jié)構(gòu)
交換前:
交換后,發(fā)現(xiàn)兩邊key不相等,于是嘗試在Element 列表里面查找是否還有相同的key的Element,發(fā)現(xiàn)有,于是重新排列Element讓相同key的配對
rebuild后,Element已交換,重新渲染后視覺上就看到兩個色塊交換位置了:
在這種加了key又交換位置的情況下,Element和widget都是直接復用的,所以點擊交換位置,widget沒有觸發(fā)build方法,原因在于canUpdate方法返回false,didUpdateWidget也沒有回調(diào),build方法也不會被觸發(fā)
接下來我們在原來的demo上做些小改動,在要交換的2個Widget外面分別套上Padding,我們看下效果:
我們發(fā)現(xiàn)每次點擊交換位置,2個Widget都變成了新的顏色,即兩個 Widget 的 Element 并不是交換順序,而是被重新創(chuàng)建了
當交換子節(jié)點的位置時,F(xiàn)lutter 的 element-to-widget 匹配邏輯一次只會檢查樹的一個層級。
在Column這一層級,padding 部分的 runtimeType 并沒有改變,且不存在 Key。Element復用,然后再比較下一個層級。由于內(nèi)部的 StatefulColorfulTile 存在 key,且現(xiàn)在的層級在 padding 內(nèi)部,該層級沒有多子 Widget。canUpdate 返回 flase,F(xiàn)lutter 將會認為這個 Element 需要被替換。然后重新生成一個新的 Element 對象裝載到 Element 樹上替換掉之前的 Element。第二個 Widget 同理。
所以為了解決這個問題,我們需要將 key 放到 Padding 的 這一層級就可以了文章來源:http://www.zghlxwxcb.cn/news/detail-480888.html
根據(jù)上面的例子我們能了解到:如果要在有狀態(tài)的、類型相同、同一層級的 widget 集合上進行添加、刪除、排序等操作,可能需要使用到 key。文章來源地址http://www.zghlxwxcb.cn/news/detail-480888.html
到了這里,關(guān)于Flutter Widget 生命周期 & key探究的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!