前情提要
大概是四月份左右,裸辭了一波。之後就一直在打遊戲、複習、面試中迴圈度日,到現在還沒有一個特別滿意的結果。
感覺自己開始往佛系的方向發展了,難道這就是大起大落後的大徹大悟嗎?
上面的話就權當開個玩笑,本篇文章的起因是在某次面試中,一位面試官問我Flutter裡跨元件通訊有哪些方式,我說的其中一種就是做一個統一管理,這樣全域性獲取後就可以跨元件通訊了,不過面試官沒有給到一個正面的反饋,所以我就打算做一個這樣的狀態管理元件出來。如果下次再有人問我這個問題,我就會告訴他——“我給你講講我寫的一個元件吧(微笑)”
下面開始正題
Flutter的重新整理流程
想要做一個狀態管理的元件,首先得了解一下Flutter的重新整理流程,在前面寫的 《從原始碼看Flutter系列》, 已經對這一過程有所瞭解,下面再簡單介紹一下
- 呼叫
setState()
後,將對應的Element
新增到BuildOwner
維護的_dirtyElements
列表中 - 等待
engine
的frame
回撥通知,會觸發WidgetsBinding
的drawFrame()
方法,然後會遍歷之前的_dirtyElements
,根據Element
在樹中的高度,由上到下呼叫其rebuild()
方法進行重新建立或更新 Element
的重新整理過程中,會將需要重新layout、paint的RenderObject
存放在PipelineOwner
維護的各個列表裡,之後會在RendererBinding
的drawFrame()
方法裡對RenderObject
來一個統一的更新- 重新整理結束後,就是通過
BuildOwner
的finalizeTree()
來進行統一的銷燬操作了
以上就是重新整理流程的一個大致介紹。通過這個流程我們知道,對於需要更新或者銷燬的物件,Flutter的做法就是放入一個列表中進行統一操作,在瞭解到這個事實後,顯然元件狀態也是可以統一管理的,這也就是後面將要實現的狀態管理元件的核心原理啦。
InheritedElement與重新整理
在正式介紹狀態管理元件之前,我還是要先介紹一下 InheritedElement
這個常見嘉賓,Flutter中的全域性主題修改等都是基於這個物件的,它對應的 Widget
是 InheritedWidget
,通過使用 InheritedWidget
,我們也可以做到跨元件通訊。不過我個人總覺得它的使用方式不太美觀,所以幾乎很少用到。
現在非常受歡迎的 provider
庫與之前的 scope_model
,都是基於 InheritedElement
來實現的,但是在使用 provider
的過程中會遇到這樣一個問題:
當你在 PageC
通過 Provider.of<ModelB>(context)
來獲取 PageB
對應的 Model
時,是會報錯的,因為獲取到的物件為null。
導致報錯其實涉及到兩個原因,分別與 InheritedElement
和頁面棧相關,下面就來簡單的說明一下。
InheritedElement的傳遞
provider中常用 Provider.of<T>(context)
來獲取對應的資料物件,最終呼叫的都是 BuildContext
中的 getElementForInheritedWidgetOfExactType
方法,它的實現如下
///Element
Map<Type, InheritedElement> _inheritedWidgets;
///InheritedElement
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
...
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
複製程式碼
查詢是通過而 _inheritedWidgets
來進行的,而它在 InheritedElement
中是如何傳遞的呢?
///InheritedElement
@override
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
複製程式碼
就是通過 copy 父節點的 _inheritedWidgets
來達到傳遞效果,這在之前的《從原始碼看Element》中就已經提到過
到這裡就知道了 InheritedElement
是如何傳遞和查詢的了,接下來我們看一下導致 provider
無法獲取物件的另外一個原因
頁面棧的結構
我們開啟和彈出一個頁面,都是通過 Navigator
來操作的,而最終所有的頁面都會被封裝到 OverlayEntryWidget
中,被新增到 _Theatre
所持有的 children
列表裡,也就是說所有的頁面在資料結構上實際是平級的關係,下面用一個簡單的圖形表示一下
因為 InheritedElement
的查詢就是通過父節點向上遍歷,直到找到指定的物件為止,否則返回null,而這裡由於 PageC 與 PageB 是平級的關係,顯然 PageC 無法找到 PageB 對應的資料(實際上是對應的Element為平級,這裡做了簡化)
這也就是使用 provider
會遇到這樣問題的原因,當然解決辦法也很簡單,就是將 Model
都放入 GlobalModel
中,通過 GlobalModel
獲取即可
上面介紹完的這些對於理解狀態管理有一定的幫助,下面就開始正式介紹我是如何實現狀態管理元件的
實現狀態管理元件
實現的思路非常簡單,就是通過維護一個 HashMap
物件,將各個頁面對應的 Model
放入其中,獲取的時候通過這個 HashMap
獲取即可。
不過可能會遇到下面這種場景:
當需要push多個相同的頁面時,會有多個同型別的 Model
物件,顯然這在 HashMap
中是無法通過型別來獲取指定 Model
的,解決辦法也很簡單,那就是再維護一個 HashMap
,而 key
由使用者指定,這樣就不必擔心衝突的問題了
原理大致就是這樣,最終程式碼如下
class ModelWidget<T extends Model> extends StatefulWidget {
final ChildBuilder<T> childBuilder;
final ModelBuilder<T> modelBuilder;
final String modelKey;
const ModelWidget(
{Key key,
@required this.childBuilder,
@required this.modelBuilder,
this.modelKey})
: super(key: key);
@override
_ModelWidgetState createState() => _ModelWidgetState<T>();
}
typedef ChildBuilder<T extends Model> = Widget Function(
BuildContext context, T model);
typedef ModelBuilder<T extends Model> = T Function();
class _ModelWidgetState<T extends Model> extends State<ModelWidget<T>> {
...
}
class Model { ... }
class _StateDelegate { ... }
class ModelGroup {
static Map<Type, Model> _map = new HashMap();
static Map<String, Model> _repeatMap = new HashMap();
static void _pushModel(Model model) => _map[model.runtimeType] = model;
static void _pushModelWithKey(String key, Model model) =>
_repeatMap[key] = model;
static void _popModel(Model model) => _map.remove(model.runtimeType);
static void _popModelWithKey(String key, Model model) => _repeatMap.remove(key);
static T findModel<T extends Model>() => _map[T];
static T findModelByKey<T extends Model>(String key) => _repeatMap[key];
}
複製程式碼
由於總共的程式碼量非常少,對細節有興趣的小夥伴可以直接去看原始碼
使用方式如下
? 使用方式
首先定義你的 Model
物件
class YourModel extends Model {
@override
void initState() {...}
@override
void dispose() {...}
int value = 0;
}
複製程式碼
當你想要把它與某個Widget或頁面結合使用時,可以像下面這樣
ModelWidget<YourModel>(
childBuilder: (ctx, model) => YourWidgetOrPage(),
modelBuilder: () => YourModel(),
),
複製程式碼
? 獲取資料與重新整理
獲取資料
final model = ModelGroup.findModel<YourModel>();
複製程式碼
重新整理
model.refresh();
複製程式碼
你也可以直接嘗試一下這個線上demo,點選體驗
最後
裸辭期間總共開源了兩個元件:
- 一個就是這個完成不久的 easy_model
- 另一個是markdown的渲染元件: markdown_widget
(主要是為了實現我用flutter寫的個人Web部落格)
同時,最後宣告一下:落魄小哥,線上求職
有好的內推機會請務必不要放過我,我的聯絡方式就在上面的部落格地址中