ProxyWidget和Element更新的正確方式詳解
正文
Flutter的眾多Widget當中,有作用于渲染的RenderObjectWidget、聚焦于功能整合的StatefulWidget。但是,還有一個大類,ProxyWidget也同樣值得我們關(guān)注。
與其相關(guān)的有兩個大類:
- InhertedWidget
- ParentDataWidget(代表:Positioned、Expanded)
這兩個Widget,無非都是數(shù)據(jù)的向下傳遞,其一InheritedWidget更多的是業(yè)務數(shù)據(jù),比如用戶的ID、購物車的條目等等,而ParentDataWidget一般都是視圖的數(shù)據(jù),Stack需要使用parentData
參數(shù)中的長寬、偏移量來完成對子Widget的定位。
所以,我們可以根據(jù)ProxyWidget的子類,向上預先給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ù)的實現(xiàn),它傳入了一個老的、支持協(xié)變的ProxyWidget進來,這意味著傳進來的應該是一個老的CustomProxyWidget
的實例,這意味著我們在notifyClients
中,可以同時拿到老的CustomProxyWidget實例和當前CustomProxyWidget實例的引用,分別是oldWidget
和this.widget
。
一新一舊,不難看出ProxyWidget的notifyClients
調(diào)用,應該是要去做一些新舊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
這一層,這么做的原因其實和BuildContext存在的意義是一樣的,讓上層應用開發(fā)者只關(guān)注Widget,而更少地去感知Element的存在。
總而言之,notifyClients
存在的作用和意義,就是通知訂閱它的子Widget,以實現(xiàn)子Widget的更新,我們也能稍稍瞥見一些ProxyWidget和ProxyElement的作用,大體上都是和數(shù)據(jù)傳輸和共享相關(guān)的。
2. InheritedWidget
基于觀察者模式的InheritedWidget
,它的使用我們就不做過多的敘述了,整體上而言,就三步走:
- 注冊:利用BuildContext注冊監(jiān)聽
- 通過BuildContext獲取數(shù)據(jù)
- 通知:改變促進監(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
當中,有一個特殊的結(jié)構(gòu),它存儲了我們上面通過context調(diào)用時的context,這樣來實現(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
,它的實現(xiàn)如下:
@mustCallSuper void didChangeDependencies() { …… markNeedsBuild(); }
至此,InheritedWidget是如何通知到子Widget進行更新的整個鏈路已經(jīng)是非常清晰了。
由于didChangedDepenedencies()的存在,只有添加了依賴的結(jié)點才會因為數(shù)據(jù)的更新而造成節(jié)點的rebuild,而不會像StatefulWidget一樣,對整棵子樹做一次完全的rebuild,這是整個ProxyWidget/ProxyElement的特性。
2.3 何時更新?
InheritedWidget自身只負責數(shù)據(jù)的向下傳遞,子Widget可以從InheritedWidget中讀出數(shù)據(jù),但是,諸如我們的子Widget中的onPressed的回調(diào)函數(shù)中,對InheritedWidget中的數(shù)據(jù)進行修改,通常情況下是無法實現(xiàn)UI的更新的,因為InheritedWidget調(diào)用notifyClients()
是有時機限制的。
僅當是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ā)生了復用的情況。比如State調(diào)用build方法構(gòu)建了一個新的Widget子樹,這個子樹中的Widget都是全新的Widget,并且如果只是修改Text對應的String中的內(nèi)容,Text對應的Element此時就會發(fā)生復用,這個過程就是Element的update()
,即 用新的newWidget替換掉舊的oldWidget的過程,可以理解為Element的配置的改變。
所以,InheritedWidget的更新就必須依賴于InheritedWidget的上層更新,比如去調(diào)用setState等等,這個觸發(fā)條件似乎有一點苛刻了,我們肯定是希望在子Widget中修改了InheritedWidget中的數(shù)據(jù)之后,就直接就能反應到視圖。
我們可以在onPressed等回調(diào)方法中,調(diào)用完修改方法之后,手動調(diào)用一下setState來手動重建Widget,也可以在InheritedWidget中自己定義一個相關(guān)的方法,傳入Context,統(tǒng)一處理。
3. ParentDataWidget
之前介紹InheritedWidget主要是講了它作為ProxyWidget,它的notifyClients
是如何實現(xiàn)的,作為ProxyWidget的另一個分支,ParentDataWidget也是一個非常常用的Widget,它的常見實現(xiàn)類包括:Flexible(常用Expanded)、Positioned
等等。它們都有一個非常明顯的特點:具有一個其父組件(Flext、Stack)需要的一個額外信息,父組件會使用這個額外的信息對當前組件進行布局、定位。
相比較于InheritedWidget,ParentDataWidget的使用場景更多的是偏向于視圖本身的數(shù)據(jù),比如尺寸、偏移量等等。
3.1 Positioned
首先我們來看看Positioned,Stack嵌套Positioned,在Positioned可以設置height/width和left/top/right/bottom等一系列的尺寸、位置屬性,我們需要關(guān)注的,是ParentDataElement對應的的notifyClients究竟干了些什么。
我們先來看看Positioned的功能。Positioned先將傳遞進來的renderObject對象中的parentData結(jié)構(gòu)取出,然后再向其中塞數(shù)據(jù),之后的布局過程中,Stack就可以根據(jù)StackParentData
中的數(shù)據(jù)進行布局了。
ParentDataElement的notifyClients方法,只調(diào)用了一個方法,我們可以快速地定位到_applyParentData方法:
@override void applyParentData(RenderObject renderObject) { assert(renderObject.parentData is StackParentData); final StackParentData parentData = renderObject.parentData! as StackParentData; …… }
這里傳進來的正是Positioned
的child屬性對應的RenderObject
,Positioned將設置的尺寸、偏移量作為一個StackParentData傳遞進去,然后再Render階段對其進行位置的確定和布局。
接下來的場景如下:Stack下面套了三個Positioned,對應三個具有顏色的Container。
Positioned本身是不參與Render的,我們可以很清楚地看到,RenderStack的child直接就是RenderColoredBox,即一個具有顏色的Box,是由Container創(chuàng)建的,而不是一個Positioned
(Container本身是一個復合型的StatelessWidget)。我們可以模糊地理解成,RenderTree下,Stack下直接就是Container。
ProxyWidget還是會存在于Element、Widget樹當中的,只是在渲染的時候,它并不是一個RenderObject節(jié)點,所以,自然而然不參與渲染,但是它的數(shù)據(jù)還存在它的孩子對應的ParentData當中。重新構(gòu)建時,也是調(diào)用renderObject.parent(在RenderTree上的parent,即Stack)進行重建
所以,ProxyWidget本身是不參與渲染的,他只作為一個中間Widget,為下層的Child對應的RenderObject,提供上層(Stack)所需要的數(shù)據(jù)(尺寸、偏移量等等)。
同為ParentDataWidget的Flexible同理,只不過把適用于Stack的StackParentData,換成了適用于Flex的FlexParentData,以StackParentData為例,我們只需要知道它的數(shù)據(jù)是記錄在Postioned的child對應的RenderObject下,交給的父布局Stack使用即可,ParentDataWidget的使命也僅限于此。
4. 后記
既然Positioned對應的Element也是ProxyElement的子類,那么它的notifyClients的調(diào)用就和InheritedWidget相同,當Element#update調(diào)用時,才會調(diào)用notifyClients,去重新為子Widget設置StackParentData(尺寸、寬高數(shù)據(jù)),然后去重新布局子Widget。
這也是ProxyElement一貫的處理方式,當ProxyWidget對應的數(shù)據(jù)發(fā)生改變(InheritedWidget一般是業(yè)務數(shù)據(jù),ParentDataWidget一般是一些視圖數(shù)據(jù)),才會去重建視圖,而Widget數(shù)據(jù)發(fā)生改變的唯一方法,就是重新創(chuàng)建一個Widget,而不是在原有的Widget上通過回調(diào)等手段來進行賦值、增減等等,這種情況并不視為Widget的改變。
從Element的角度來說,如果Widget想要改變就必然要通過Element#update方法,即使是StatefulWidget,它的改變也是從State調(diào)用setState開始,然后StatefulWidget去rebuild一個新的Child Widget子樹,再調(diào)用Element的update方法,將新的子樹掛載上來完成新舊數(shù)據(jù)的更迭。
簡單來說,默認情況下,數(shù)據(jù)的變更必須精確到Widget層面,Element才有可能看得見。
一旦認為數(shù)據(jù)發(fā)生了改變,那么ProxyElement則會通過notifyClients方法,通知所有的監(jiān)聽者,監(jiān)聽者此時的行為:
- 如果是InheritedWidget,那么就是調(diào)用監(jiān)聽者的didChangeDependencies,重建監(jiān)聽者對應的視圖。
- 如果是ParentDataWidget,那么就是調(diào)用ParentDataElement的applyParentData函數(shù),去重新build它的子集。
以上就是ProxyWidget和Element更新的正確方式詳解的詳細內(nèi)容,更多關(guān)于ProxyWidget Element更新的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Flutter彈性布局Flex水平排列Row垂直排列Column使用示例
這篇文章主要為大家介紹了Flutter彈性布局Flex水平排列Row垂直排列Column使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08Android應用中clearFocus方法調(diào)用無效的問題解決
clearFocus()主要用于清除EditText的焦點,Android App開發(fā)中很多時候會發(fā)現(xiàn)其調(diào)用無效,帶著這個問題我們就來看一下本文主題、Android應用中clearFocus方法調(diào)用無效的問題解決2016-05-05Android ListView用EditText實現(xiàn)搜索功能效果
本篇文章主要介紹了Android ListView用EditText實現(xiàn)搜索功能效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03XrecyclerView實現(xiàn)加載數(shù)據(jù)和切換不同布局
這篇文章主要為大家詳細介紹了XrecyclerView實現(xiàn)加載數(shù)據(jù)、切換不同布局功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12Android開發(fā)必知 九種對話框的實現(xiàn)方法
App中少不了與用戶交互的各種dialog,以此達到很好的用戶體驗,下面給大家介紹Android開發(fā)必知 九種對話框的實現(xiàn)方法,有需要的朋友可以參考下2015-08-08