ProxyWidget和Element更新的正確方式詳解
正文
Flutter的眾多Widget當(dāng)中,有作用于渲染的RenderObjectWidget、聚焦于功能整合的StatefulWidget。但是,還有一個大類,ProxyWidget也同樣值得我們關(guān)注。

與其相關(guān)的有兩個大類:
- InhertedWidget
- ParentDataWidget(代表:Positioned、Expanded)
這兩個Widget,無非都是數(shù)據(jù)的向下傳遞,其一InheritedWidget更多的是業(yè)務(wù)數(shù)據(jù),比如用戶的ID、購物車的條目等等,而ParentDataWidget一般都是視圖的數(shù)據(jù),Stack需要使用parentData參數(shù)中的長寬、偏移量來完成對子Widget的定位。
所以,我們可以根據(jù)ProxyWidget的子類,向上預(yù)先給ProxyWidget扣一個數(shù)據(jù)共享的「帽子」。
1. ProxyWidget和ProxyElement的主要功能
ProxyWidget本身是抽象的,需要我們重寫它的createElement()方法:
class CustomProxyWidget extends ProxyWidget {
const CustomProxyWidget({required Widget child}) : super(child: child);
@override
Element createElement() => CustomProxyElement(this);
}
而ProxyElement則要重寫notifyClients方法。
class CustomProxyElement extends ProxyElement {
CustomProxyElement(ProxyWidget widget) : super(widget);
@override
void notifyClients(covariant ProxyWidget oldWidget) {
//......
}
}
整個ProxyElement的關(guān)鍵代碼,就notifyClients這個函數(shù)的實(shí)現(xiàn),它傳入了一個老的、支持協(xié)變的ProxyWidget進(jìn)來,這意味著傳進(jìn)來的應(yīng)該是一個老的CustomProxyWidget的實(shí)例,這意味著我們在notifyClients中,可以同時拿到老的CustomProxyWidget實(shí)例和當(dāng)前CustomProxyWidget實(shí)例的引用,分別是oldWidget和this.widget。
一新一舊,不難看出ProxyWidget的notifyClients調(diào)用,應(yīng)該是要去做一些新舊Widget的數(shù)據(jù)比較而存在的。
比如,我們可以這樣重寫它:
@override
void notifyClients(covariant ProxyWidget oldWidget) {
if((oldWidget as CustomProxyWidget).data != (widget as CustomProxyWidget).data){
// 通知所有訂閱者,數(shù)據(jù)變動了
_clients.foreach((e)=>e.notify());
}
}
我們可以根據(jù)data屬性(data是CustomProxyWidget新增的一個int類型的字段)的變化,來決定是否需要通知訂閱者的Element是否去重新繪制子Widget,一旦data發(fā)生了變化,那么就去遍歷_clients中的數(shù)據(jù),并調(diào)用e.notify操作監(jiān)聽者重新繪制視圖。
這讓我們不禁和InheritedWidget的updateShouldNotify聯(lián)系起來,簡單分析一下updateShouldNotify的調(diào)用鏈條:
InhertiedElement#update -> updateShouldNotify() 判斷是否需要更新數(shù)據(jù) InhertiedElement#update -> callsuper 即調(diào)用ProxyElement的update方法 ProxyElement#update -> notifyClients();
顯然,InheritedWidget將notifyClients做了一個封裝updateShouldNotify,并把這個封裝放在Widget層,而不是直接讓開發(fā)者去重寫notifyClients這一層,這么做的原因其實(shí)和BuildContext存在的意義是一樣的,讓上層應(yīng)用開發(fā)者只關(guān)注Widget,而更少地去感知Element的存在。
總而言之,notifyClients存在的作用和意義,就是通知訂閱它的子Widget,以實(shí)現(xiàn)子Widget的更新,我們也能稍稍瞥見一些ProxyWidget和ProxyElement的作用,大體上都是和數(shù)據(jù)傳輸和共享相關(guān)的。
2. InheritedWidget
基于觀察者模式的InheritedWidget,它的使用我們就不做過多的敘述了,整體上而言,就三步走:
- 注冊:利用BuildContext注冊監(jiān)聽
- 通過BuildContext獲取數(shù)據(jù)
- 通知:改變促進(jìn)監(jiān)聽者的數(shù)據(jù)重繪
這是一個非常典型的觀察者模式的使用步驟,只不過InheritedWidget為我們做了一些封裝,「注冊」、「通知」操作變得更加地“隱蔽”了。
2.1 注冊
使用InheritedWidget時,我們并沒有手動地調(diào)用addListener、addObserver這類的方法,去主動添加監(jiān)聽,這一切都是無感的。我們一般通過如下方法獲取到InheritedWidget中的數(shù)據(jù)。
context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
這一行代碼已經(jīng)包括兩個步驟了:注冊監(jiān)聽和獲取數(shù)據(jù)。
在InheritedElement當(dāng)中,有一個特殊的結(jié)構(gòu),它存儲了我們上面通過context調(diào)用時的context,這樣來實(shí)現(xiàn)注冊的監(jiān)聽,并且,在注冊完成之后,會將所需要的數(shù)據(jù)返回給調(diào)用者,這樣一來,監(jiān)聽注冊、數(shù)據(jù)的獲取這一個操作就合二為一了。
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
2.2 通知
對于StatefulWidget的重繪,我們一定會想到一個方法:markNeedsBuild(),所以,我們就順著上述的調(diào)用,查找是否有相關(guān)的調(diào)用,我們可以看看屬于InheritedElement的notifyClients的調(diào)用鏈:
InheritedElement# notifyClients InheritedElement# notifyDependent(oldWidget, dependent); dependent#didChangeDependencies();
一路從notifyClients調(diào)用到_dependents中的某個dependent的didChangeDependencies方法,這就是通知的整個流程,InheritedWidget通過這樣的調(diào)用,通知所有掛載著的監(jiān)聽者,即其他需要InheritedWidget數(shù)據(jù)的Widget的BuildContext,并調(diào)用BuildContext的didChangeDependencies,它的實(shí)現(xiàn)如下:
@mustCallSuper
void didChangeDependencies() {
……
markNeedsBuild();
}
至此,InheritedWidget是如何通知到子Widget進(jìn)行更新的整個鏈路已經(jīng)是非常清晰了。
由于didChangedDepenedencies()的存在,只有添加了依賴的結(jié)點(diǎn)才會因?yàn)閿?shù)據(jù)的更新而造成節(jié)點(diǎn)的rebuild,而不會像StatefulWidget一樣,對整棵子樹做一次完全的rebuild,這是整個ProxyWidget/ProxyElement的特性。
2.3 何時更新?
InheritedWidget自身只負(fù)責(zé)數(shù)據(jù)的向下傳遞,子Widget可以從InheritedWidget中讀出數(shù)據(jù),但是,諸如我們的子Widget中的onPressed的回調(diào)函數(shù)中,對InheritedWidget中的數(shù)據(jù)進(jìn)行修改,通常情況下是無法實(shí)現(xiàn)UI的更新的,因?yàn)镮nheritedWidget調(diào)用notifyClients()是有時機(jī)限制的。
僅當(dāng)是ProxyElement#update()被調(diào)用時,才會調(diào)用updateShouldNotify()去評估是否要調(diào)用notifyClients去更新布局。而一般都數(shù)據(jù)修改,例如int++、String賦值等等并不能觸發(fā)notifyClients調(diào)用。
所以,只有Element#update()方法調(diào)用時,才能驅(qū)動子Widget發(fā)生視圖更新,而Element#update()方法僅在:Element不變,Widget發(fā)生改變的時候才會觸發(fā),常見于Widget作為一個配置,發(fā)生了改變,而Element發(fā)生了復(fù)用的情況。比如State調(diào)用build方法構(gòu)建了一個新的Widget子樹,這個子樹中的Widget都是全新的Widget,并且如果只是修改Text對應(yīng)的String中的內(nèi)容,Text對應(yīng)的Element此時就會發(fā)生復(fù)用,這個過程就是Element的update(),即 用新的newWidget替換掉舊的oldWidget的過程,可以理解為Element的配置的改變。

所以,InheritedWidget的更新就必須依賴于InheritedWidget的上層更新,比如去調(diào)用setState等等,這個觸發(fā)條件似乎有一點(diǎn)苛刻了,我們肯定是希望在子Widget中修改了InheritedWidget中的數(shù)據(jù)之后,就直接就能反應(yīng)到視圖。
我們可以在onPressed等回調(diào)方法中,調(diào)用完修改方法之后,手動調(diào)用一下setState來手動重建Widget,也可以在InheritedWidget中自己定義一個相關(guān)的方法,傳入Context,統(tǒng)一處理。
3. ParentDataWidget
之前介紹InheritedWidget主要是講了它作為ProxyWidget,它的notifyClients是如何實(shí)現(xiàn)的,作為ProxyWidget的另一個分支,ParentDataWidget也是一個非常常用的Widget,它的常見實(shí)現(xiàn)類包括:Flexible(常用Expanded)、Positioned等等。它們都有一個非常明顯的特點(diǎn):具有一個其父組件(Flext、Stack)需要的一個額外信息,父組件會使用這個額外的信息對當(dāng)前組件進(jìn)行布局、定位。
相比較于InheritedWidget,ParentDataWidget的使用場景更多的是偏向于視圖本身的數(shù)據(jù),比如尺寸、偏移量等等。
3.1 Positioned
首先我們來看看Positioned,Stack嵌套Positioned,在Positioned可以設(shè)置height/width和left/top/right/bottom等一系列的尺寸、位置屬性,我們需要關(guān)注的,是ParentDataElement對應(yīng)的的notifyClients究竟干了些什么。
我們先來看看Positioned的功能。Positioned先將傳遞進(jìn)來的renderObject對象中的parentData結(jié)構(gòu)取出,然后再向其中塞數(shù)據(jù),之后的布局過程中,Stack就可以根據(jù)StackParentData中的數(shù)據(jù)進(jìn)行布局了。
ParentDataElement的notifyClients方法,只調(diào)用了一個方法,我們可以快速地定位到_applyParentData方法:
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is StackParentData);
final StackParentData parentData = renderObject.parentData! as StackParentData;
……
}
這里傳進(jìn)來的正是Positioned的child屬性對應(yīng)的RenderObject,Positioned將設(shè)置的尺寸、偏移量作為一個StackParentData傳遞進(jìn)去,然后再Render階段對其進(jìn)行位置的確定和布局。
接下來的場景如下:Stack下面套了三個Positioned,對應(yīng)三個具有顏色的Container。
Positioned本身是不參與Render的,我們可以很清楚地看到,RenderStack的child直接就是RenderColoredBox,即一個具有顏色的Box,是由Container創(chuàng)建的,而不是一個Positioned(Container本身是一個復(fù)合型的StatelessWidget)。我們可以模糊地理解成,RenderTree下,Stack下直接就是Container。

ProxyWidget還是會存在于Element、Widget樹當(dāng)中的,只是在渲染的時候,它并不是一個RenderObject節(jié)點(diǎn),所以,自然而然不參與渲染,但是它的數(shù)據(jù)還存在它的孩子對應(yīng)的ParentData當(dāng)中。重新構(gòu)建時,也是調(diào)用renderObject.parent(在RenderTree上的parent,即Stack)進(jìn)行重建

所以,ProxyWidget本身是不參與渲染的,他只作為一個中間Widget,為下層的Child對應(yīng)的RenderObject,提供上層(Stack)所需要的數(shù)據(jù)(尺寸、偏移量等等)。

同為ParentDataWidget的Flexible同理,只不過把適用于Stack的StackParentData,換成了適用于Flex的FlexParentData,以StackParentData為例,我們只需要知道它的數(shù)據(jù)是記錄在Postioned的child對應(yīng)的RenderObject下,交給的父布局Stack使用即可,ParentDataWidget的使命也僅限于此。
4. 后記
既然Positioned對應(yīng)的Element也是ProxyElement的子類,那么它的notifyClients的調(diào)用就和InheritedWidget相同,當(dāng)Element#update調(diào)用時,才會調(diào)用notifyClients,去重新為子Widget設(shè)置StackParentData(尺寸、寬高數(shù)據(jù)),然后去重新布局子Widget。
這也是ProxyElement一貫的處理方式,當(dāng)ProxyWidget對應(yīng)的數(shù)據(jù)發(fā)生改變(InheritedWidget一般是業(yè)務(wù)數(shù)據(jù),ParentDataWidget一般是一些視圖數(shù)據(jù)),才會去重建視圖,而Widget數(shù)據(jù)發(fā)生改變的唯一方法,就是重新創(chuàng)建一個Widget,而不是在原有的Widget上通過回調(diào)等手段來進(jìn)行賦值、增減等等,這種情況并不視為Widget的改變。
從Element的角度來說,如果Widget想要改變就必然要通過Element#update方法,即使是StatefulWidget,它的改變也是從State調(diào)用setState開始,然后StatefulWidget去rebuild一個新的Child Widget子樹,再調(diào)用Element的update方法,將新的子樹掛載上來完成新舊數(shù)據(jù)的更迭。
簡單來說,默認(rèn)情況下,數(shù)據(jù)的變更必須精確到Widget層面,Element才有可能看得見。
一旦認(rèn)為數(shù)據(jù)發(fā)生了改變,那么ProxyElement則會通過notifyClients方法,通知所有的監(jiān)聽者,監(jiān)聽者此時的行為:
- 如果是InheritedWidget,那么就是調(diào)用監(jiān)聽者的didChangeDependencies,重建監(jiān)聽者對應(yīng)的視圖。
- 如果是ParentDataWidget,那么就是調(diào)用ParentDataElement的applyParentData函數(shù),去重新build它的子集。
以上就是ProxyWidget和Element更新的正確方式詳解的詳細(xì)內(nèi)容,更多關(guān)于ProxyWidget Element更新的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Flutter彈性布局Flex水平排列Row垂直排列Column使用示例
這篇文章主要為大家介紹了Flutter彈性布局Flex水平排列Row垂直排列Column使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Android應(yīng)用中clearFocus方法調(diào)用無效的問題解決
clearFocus()主要用于清除EditText的焦點(diǎn),Android App開發(fā)中很多時候會發(fā)現(xiàn)其調(diào)用無效,帶著這個問題我們就來看一下本文主題、Android應(yīng)用中clearFocus方法調(diào)用無效的問題解決2016-05-05
Android ListView用EditText實(shí)現(xiàn)搜索功能效果
本篇文章主要介紹了Android ListView用EditText實(shí)現(xiàn)搜索功能效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
Android中實(shí)現(xiàn)圓角圖片的幾種方法
本篇文章主要介紹了Android中實(shí)現(xiàn)圓角圖片的幾種方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06
XrecyclerView實(shí)現(xiàn)加載數(shù)據(jù)和切換不同布局
這篇文章主要為大家詳細(xì)介紹了XrecyclerView實(shí)現(xiàn)加載數(shù)據(jù)、切換不同布局功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
Android開發(fā)必知 九種對話框的實(shí)現(xiàn)方法
App中少不了與用戶交互的各種dialog,以此達(dá)到很好的用戶體驗(yàn),下面給大家介紹Android開發(fā)必知 九種對話框的實(shí)現(xiàn)方法,有需要的朋友可以參考下2015-08-08

