1、為什么需要局部刷新
如下圖場景:在一個(gè)Navigator的某Router上有個(gè)Scffold頁面怠苔,頁面上并列三個(gè)StatefulWidget得封,分別是A埋心、B、C忙上。
此時(shí)此頁面對(duì)應(yīng)的Tree應(yīng)為右圖所示拷呆。
問題:當(dāng)A節(jié)點(diǎn)上顯示的某文本需要變化,怎么操作晨横,才是最好的選擇呢洋腮?
回答:這種場景很多,將A節(jié)點(diǎn)的文案對(duì)象放入State屬性中手形,修改為新的文本啥供,調(diào)用setStates()方法即可。那么怎么才是更高效的需要理解setStates()方法做了哪些库糠。
2伙狐、setStates()做了什么呢?
簡單的說就是將setStates的Widget對(duì)象對(duì)應(yīng)的Element對(duì)象標(biāo)記為dirty(臟的瞬欧,意思是需要刷新的)贷屎,并將其存儲(chǔ)到了一個(gè)全局的鏈表中。然后就是等待艘虎,等待什么呢唉侄?等待系統(tǒng)下一幀的Vsync通知,當(dāng)系統(tǒng)告知我們下一幀可以顯示了野建,widgetBinding就會(huì)找到這個(gè)存放著需要刷新element的鏈表重新繪制属划。
所以我們暫且理解為:誰(這里理解為某個(gè)StatefulWidget實(shí)例)調(diào)用了setStates(),誰就會(huì)執(zhí)行build()方法候生,并重新繪制同眯。
所以如果只需要A需要重繪,只需要A調(diào)用setStates()即可唯鸭,那么具體代碼我們怎么寫呢须蜗?
3、具體代碼書寫
1、錯(cuò)誤示范:(整個(gè)頁面刷新)
String a_test;
String b_test;
String c_test;
@override
void initState() {
super.initState();
a_test = 'A';
b_test = 'B';
c_test = 'C';
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(a_test),
Text(b_test),
Text(c_test),
GestureDetector(
onTap: () {
a_test = 'A_NEW';
setState(() {});
},
child: Text('點(diǎn)擊修改A的文案'),
)
],
);
}
這種情況會(huì)導(dǎo)致整個(gè)頁面的build()被執(zhí)行明肮,那么Column以及Column中的四個(gè)child都會(huì)重新創(chuàng)建菱农,如果是復(fù)雜頁面,將會(huì)出現(xiàn)卡幀晤愧,這無疑違背高效原則大莫。
2、一個(gè)比較實(shí)在的正確寫法:
既然只需要A刷新官份,那么我們把A單獨(dú)抽成一個(gè)類并集成于StatefulWidget只厘,我們只在A類中做setStates():
父節(jié)點(diǎn):
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_AText(
key: aKey,
),
Text(b_test),
Text(c_test),
GestureDetector(
onTap: () {
__ATextState astate = aKey.currentState;
astate.updateText();
},
child: Text('點(diǎn)擊修改A的文案'),
)
],
);
}
A節(jié)點(diǎn):
String a_test;
@override
void initState() {
super.initState();
a_test = 'A';
}
@override
Widget build(BuildContext context) {
return Text(a_test);
}
void updateText() {
a_test = 'A_NEW';
setState(() {});
}
這種寫法,經(jīng)過測試可以達(dá)到只有A在build舅巷,但是書寫臃腫羔味,違背優(yōu)雅原則。
3钠右、利用通知的方式
1赋元、ValueNotifier的方式:
@override
void initState() {
super.initState();
a_test = 'A';
b_test = 'B';
c_test = 'C';
a_value_noti = ValueNotifier<String>(a_test);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ValueListenableBuilder(
valueListenable: a_value_noti,
builder: (BuildContext context, String value, Widget child) {
return Text(value);
}),
Text(b_test),
Text(c_test),
GestureDetector(
onTap: () {
a_value_noti.value = 'A_NEW';
},
child: Text('點(diǎn)擊修改A的文案'),
)
],
);
}
2、Stream的方式
String a_test;
String b_test;
String c_test;
StreamController<String> aStreamC;
@override
void initState() {
super.initState();
a_test = 'A';
b_test = 'B';
c_test = 'C';
aStreamC = StreamController<String>();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
StreamBuilder(
stream: aStreamC.stream,
initialData: a_test,
builder: (context, AsyncSnapshot snapshot) {
return Text(snapshot.data);
}),
Text(b_test),
Text(c_test),
GestureDetector(
onTap: () {
aStreamC.add('A_NEW');
},
child: Text('點(diǎn)擊修改A的文案'),
)
],
);
}
4飒房、數(shù)據(jù)共享的方式
1搁凸、使用InHeritedWidget:
InHeritedWidget的使用固定,可用于共享state狠毯,如下面的使用:
- 創(chuàng)建自己的InHeritedWidget類:
class MyInheritedWidget extends InheritedWidget {
final String aText;
MyInheritedWidget(this.aText, Widget child) : super(child: child);
static MyInheritedWidget of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
return context.findAncestorWidgetOfExactType<MyInheritedWidget>();
}
@override
bool updateShouldNotify(MyInheritedWidget old) {
return aText != old.aText;
}
}
- 在需要使用到共享數(shù)據(jù)的父試圖層护糖,用我們創(chuàng)建的InHeritedWidget包裹:
return MyInheritedWidget(
a_test,
Column(
children: <Widget>[
_AText(),
_AText(),
Text(c_test),
GestureDetector(
onTap: () {
setState(() {
this.a_test = 'bbb';
});
},
child: Text('點(diǎn)擊修改A的文案'),
)
],
));
- 使用到共享數(shù)據(jù)的widget,可以使用InHeritedWidget.of(context)取得對(duì)應(yīng)屬性:
Widget build(BuildContext context) {
return Text(MyInheritedWidget.of(context, rebuild: false).aText);
}
2嚼松、使用Provider
Provider其實(shí)就是對(duì)InHeritedWidget的封裝嫡良,用法固定,感興趣的同學(xué)可以自行查找献酗,本篇不做介紹寝受。
4、對(duì)比以上方式罕偎,總結(jié)使用場景
1很澄、上述比較實(shí)在的寫法的總結(jié):
缺點(diǎn):用到了Key取到state,用法詭異颜及,可閱讀性很差痴怨。
優(yōu)點(diǎn):對(duì)復(fù)雜頁面撤防,我們可以借鑒把child分開定義的方式書寫忆肾,可以使一個(gè)復(fù)雜頁面分模塊管理挪丢。
2、通知的方式:
缺點(diǎn):數(shù)據(jù)單一乾翔,不太方便使用在復(fù)雜頁面,多數(shù)據(jù)量的存儲(chǔ),當(dāng)然我們可以把多個(gè)數(shù)據(jù)包裝成一個(gè)bean反浓,或者定義多個(gè)通知對(duì)象萌丈,但是這種用法違背我們的初衷。
優(yōu)點(diǎn):使用簡單雷则,對(duì)于基本數(shù)據(jù)類型的更新方式辆雾,有很大的優(yōu)勢。
3月劈、使用共享數(shù)據(jù)的方式:
缺點(diǎn):框架量級(jí)較大度迂,尤其是provider,雖然使用方式比較固定猜揪,但是也沒有通知的方式那么便捷惭墓。
優(yōu)點(diǎn):可實(shí)現(xiàn)多級(jí)頁面或單級(jí)多層次頁面的數(shù)據(jù)共享,使用后代碼結(jié)構(gòu)清晰而姐,閱讀性和可擴(kuò)展性強(qiáng)腊凶。
5、補(bǔ)充知識(shí)點(diǎn)拴念,RepaintBoundary的使用:
為什么使用RepaintBoundary钧萍?
上面我們說到setStates只作用于setStates的調(diào)用方及其子視圖。但我在renderObject --> markneedsPaint方法中發(fā)現(xiàn)政鼠,調(diào)用方的父試圖雖然未執(zhí)行build方法做重繪风瘦,但是系統(tǒng)卻在遍歷比較父試圖是否需要build:
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) { // 如果這個(gè)屬性為ture,則不會(huì)繼續(xù)遞歸執(zhí)行父試圖是否需要刷新的方法
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {// 如果isRepaintBoundary為false缔俄,就是尋找其父試圖執(zhí)行markNeedsPaint方法弛秋,會(huì)一直遞歸到Router。
final RenderObject parent = this.parent;
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
if (owner != null)
owner.requestVisualUpdate();
}
}
上述我們得到結(jié)論俐载,在子視圖是獨(dú)立展示且絕對(duì)不會(huì)影響到父試圖的場景下(如屏幕上的stack蟹略,stack中Position的變化時(shí),下層頁面其實(shí)無需變化)遏佣,我們可以將這個(gè)子視圖用RepaintBoundary包裹起來挖炬。
這樣則是最優(yōu)雅和高效的寫法。
點(diǎn)贊状婶、關(guān)注意敛、評(píng)論三連走一波。