網站首頁 編程語言 正文
正文
Flutter的眾多Widget當中,有作用于渲染的RenderObjectWidget、聚焦于功能整合的StatefulWidget。但是,還有一個大類,ProxyWidget也同樣值得我們關注。
與其相關的有兩個大類:
- InhertedWidget
- ParentDataWidget(代表:Positioned、Expanded)
這兩個Widget,無非都是數據的向下傳遞,其一InheritedWidget更多的是業務數據,比如用戶的ID、購物車的條目等等,而ParentDataWidget一般都是視圖的數據,Stack需要使用parentData
參數中的長寬、偏移量來完成對子Widget的定位。
所以,我們可以根據ProxyWidget的子類,向上預先給ProxyWidget扣一個數據共享的「帽子」。
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的關鍵代碼,就notifyClients
這個函數的實現,它傳入了一個老的、支持協變的ProxyWidget進來,這意味著傳進來的應該是一個老的CustomProxyWidget
的實例,這意味著我們在notifyClients
中,可以同時拿到老的CustomProxyWidget實例和當前CustomProxyWidget實例的引用,分別是oldWidget
和this.widget
。
一新一舊,不難看出ProxyWidget的notifyClients
調用,應該是要去做一些新舊Widget的數據比較而存在的。
比如,我們可以這樣重寫它:
@override void notifyClients(covariant ProxyWidget oldWidget) { if((oldWidget as CustomProxyWidget).data != (widget as CustomProxyWidget).data){ // 通知所有訂閱者,數據變動了 _clients.foreach((e)=>e.notify()); } }
我們可以根據data屬性(data是CustomProxyWidget新增的一個int類型的字段)的變化,來決定是否需要通知訂閱者的Element是否去重新繪制子Widget,一旦data發生了變化,那么就去遍歷_clients中的數據,并調用e.notify
操作監聽者重新繪制視圖。
這讓我們不禁和InheritedWidget
的updateShouldNotify
聯系起來,簡單分析一下updateShouldNotify
的調用鏈條:
InhertiedElement#update -> updateShouldNotify() 判斷是否需要更新數據 InhertiedElement#update -> callsuper 即調用ProxyElement的update方法 ProxyElement#update -> notifyClients();
顯然,InheritedWidget將notifyClients做了一個封裝updateShouldNotify
,并把這個封裝放在Widget層,而不是直接讓開發者去重寫notifyClients
這一層,這么做的原因其實和BuildContext存在的意義是一樣的,讓上層應用開發者只關注Widget,而更少地去感知Element的存在。
總而言之,notifyClients
存在的作用和意義,就是通知訂閱它的子Widget,以實現子Widget的更新,我們也能稍稍瞥見一些ProxyWidget和ProxyElement的作用,大體上都是和數據傳輸和共享相關的。
2. InheritedWidget
基于觀察者模式的InheritedWidget
,它的使用我們就不做過多的敘述了,整體上而言,就三步走:
- 注冊:利用BuildContext注冊監聽
- 通過BuildContext獲取數據
- 通知:改變促進監聽者的數據重繪
這是一個非常典型的觀察者模式的使用步驟,只不過InheritedWidget為我們做了一些封裝,「注冊」、「通知」操作變得更加地“隱蔽”了。
2.1 注冊
使用InheritedWidget
時,我們并沒有手動地調用addListener、addObserver這類的方法,去主動添加監聽,這一切都是無感的。我們一般通過如下方法獲取到InheritedWidget中的數據。
context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
這一行代碼已經包括兩個步驟了:注冊監聽和獲取數據。
在InheritedElement
當中,有一個特殊的結構,它存儲了我們上面通過context調用時的context,這樣來實現注冊的監聽,并且,在注冊完成之后,會將所需要的數據返回給調用者,這樣一來,監聽注冊、數據的獲取這一個操作就合二為一了。
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
2.2 通知
對于StatefulWidget的重繪,我們一定會想到一個方法:markNeedsBuild()
,所以,我們就順著上述的調用,查找是否有相關的調用,我們可以看看屬于InheritedElement的notifyClients
的調用鏈:
InheritedElement# notifyClients InheritedElement# notifyDependent(oldWidget, dependent); dependent#didChangeDependencies();
一路從notifyClients
調用到_dependents
中的某個dependent
的didChangeDependencies
方法,這就是通知的整個流程,InheritedWidget通過這樣的調用,通知所有掛載著的監聽者,即其他需要InheritedWidget數據的Widget的BuildContext,并調用BuildContext的didChangeDependencies
,它的實現如下:
@mustCallSuper void didChangeDependencies() { …… markNeedsBuild(); }
至此,InheritedWidget是如何通知到子Widget進行更新的整個鏈路已經是非常清晰了。
由于didChangedDepenedencies()的存在,只有添加了依賴的結點才會因為數據的更新而造成節點的rebuild,而不會像StatefulWidget一樣,對整棵子樹做一次完全的rebuild,這是整個ProxyWidget/ProxyElement的特性。
2.3 何時更新?
InheritedWidget自身只負責數據的向下傳遞,子Widget可以從InheritedWidget中讀出數據,但是,諸如我們的子Widget中的onPressed的回調函數中,對InheritedWidget中的數據進行修改,通常情況下是無法實現UI的更新的,因為InheritedWidget調用notifyClients()
是有時機限制的。
僅當是ProxyElement#update()
被調用時,才會調用updateShouldNotify()
去評估是否要調用notifyClients
去更新布局。而一般都數據修改,例如int++
、String賦值
等等并不能觸發notifyClients
調用。
所以,只有Element#update()
方法調用時,才能驅動子Widget發生視圖更新,而Element#update()
方法僅在:Element不變,Widget發生改變的時候才會觸發,常見于Widget作為一個配置,發生了改變,而Element發生了復用的情況。比如State調用build方法構建了一個新的Widget子樹,這個子樹中的Widget都是全新的Widget,并且如果只是修改Text對應的String中的內容,Text對應的Element此時就會發生復用,這個過程就是Element的update()
,即 用新的newWidget替換掉舊的oldWidget的過程,可以理解為Element的配置的改變。
所以,InheritedWidget的更新就必須依賴于InheritedWidget的上層更新,比如去調用setState等等,這個觸發條件似乎有一點苛刻了,我們肯定是希望在子Widget中修改了InheritedWidget中的數據之后,就直接就能反應到視圖。
我們可以在onPressed等回調方法中,調用完修改方法之后,手動調用一下setState來手動重建Widget,也可以在InheritedWidget中自己定義一個相關的方法,傳入Context,統一處理。
3. ParentDataWidget
之前介紹InheritedWidget主要是講了它作為ProxyWidget,它的notifyClients
是如何實現的,作為ProxyWidget的另一個分支,ParentDataWidget也是一個非常常用的Widget,它的常見實現類包括:Flexible(常用Expanded)、Positioned
等等。它們都有一個非常明顯的特點:具有一個其父組件(Flext、Stack)需要的一個額外信息,父組件會使用這個額外的信息對當前組件進行布局、定位。
相比較于InheritedWidget,ParentDataWidget的使用場景更多的是偏向于視圖本身的數據,比如尺寸、偏移量等等。
3.1 Positioned
首先我們來看看Positioned,Stack嵌套Positioned,在Positioned可以設置height/width和left/top/right/bottom等一系列的尺寸、位置屬性,我們需要關注的,是ParentDataElement對應的的notifyClients究竟干了些什么。
我們先來看看Positioned的功能。Positioned先將傳遞進來的renderObject對象中的parentData結構取出,然后再向其中塞數據,之后的布局過程中,Stack就可以根據StackParentData
中的數據進行布局了。
ParentDataElement的notifyClients方法,只調用了一個方法,我們可以快速地定位到_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創建的,而不是一個Positioned
(Container本身是一個復合型的StatelessWidget)。我們可以模糊地理解成,RenderTree下,Stack下直接就是Container。
ProxyWidget還是會存在于Element、Widget樹當中的,只是在渲染的時候,它并不是一個RenderObject節點,所以,自然而然不參與渲染,但是它的數據還存在它的孩子對應的ParentData當中。重新構建時,也是調用renderObject.parent(在RenderTree上的parent,即Stack)進行重建
所以,ProxyWidget本身是不參與渲染的,他只作為一個中間Widget,為下層的Child對應的RenderObject,提供上層(Stack)所需要的數據(尺寸、偏移量等等)。
同為ParentDataWidget的Flexible同理,只不過把適用于Stack的StackParentData,換成了適用于Flex的FlexParentData,以StackParentData為例,我們只需要知道它的數據是記錄在Postioned的child對應的RenderObject下,交給的父布局Stack使用即可,ParentDataWidget的使命也僅限于此。
4. 后記
既然Positioned對應的Element也是ProxyElement的子類,那么它的notifyClients的調用就和InheritedWidget相同,當Element#update調用時,才會調用notifyClients,去重新為子Widget設置StackParentData(尺寸、寬高數據),然后去重新布局子Widget。
這也是ProxyElement一貫的處理方式,當ProxyWidget對應的數據發生改變(InheritedWidget一般是業務數據,ParentDataWidget一般是一些視圖數據),才會去重建視圖,而Widget數據發生改變的唯一方法,就是重新創建一個Widget,而不是在原有的Widget上通過回調等手段來進行賦值、增減等等,這種情況并不視為Widget的改變。
從Element的角度來說,如果Widget想要改變就必然要通過Element#update方法,即使是StatefulWidget,它的改變也是從State調用setState開始,然后StatefulWidget去rebuild一個新的Child Widget子樹,再調用Element的update方法,將新的子樹掛載上來完成新舊數據的更迭。
簡單來說,默認情況下,數據的變更必須精確到Widget層面,Element才有可能看得見。
一旦認為數據發生了改變,那么ProxyElement則會通過notifyClients方法,通知所有的監聽者,監聽者此時的行為:
- 如果是InheritedWidget,那么就是調用監聽者的didChangeDependencies,重建監聽者對應的視圖。
- 如果是ParentDataWidget,那么就是調用ParentDataElement的applyParentData函數,去重新build它的子集。
原文鏈接:https://juejin.cn/post/7127612090050674724
相關推薦
- 2022-07-04 Python自動化辦公之清理重復文件詳解_python
- 2022-05-23 一起來學習C++的函數指針和函數對象_C 語言
- 2022-05-13 hiveserver2 連接報:root is not allowed to impersonate
- 2022-10-19 Python基礎之類的定義和使用詳解_python
- 2022-05-13 Android 10 讀寫文件權限
- 2024-01-28 使用element-ui代碼沒有提示
- 2022-06-09 FreeRTOS軟件定時器apollo中斷狀態判斷_操作系統
- 2022-09-08 Go語言中的包Package詳解_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支