日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

ProxyWidget和Element更新的正確方式詳解_Android

作者:開中斷 ? 更新時間: 2023-03-16 編程語言

正文

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實例的引用,分別是oldWidgetthis.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操作監聽者重新繪制視圖。

這讓我們不禁和InheritedWidgetupdateShouldNotify聯系起來,簡單分析一下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中的某個dependentdidChangeDependencies方法,這就是通知的整個流程,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

欄目分類
最近更新