大概是四月份左右,裸辭了一波。以後就一直在打遊戲、複習、面試中循環度日,到如今尚未一個特別滿意的結果。git
感受本身開始往佛系的方向發展了,難道這就是大起大落後的大徹大悟嗎?github
上面的話就權當開個玩笑,本篇文章的原由是在某次面試中,一位面試官問我Flutter裏跨組件通訊有哪些方式,我說的其中一種就是作一個統一管理,這樣全局獲取後就能夠跨組件通訊了,不過面試官沒有給到一個正面的反饋,因此我就打算作一個這樣的狀態管理組件出來。若是下次再有人問我這個問題,我就會告訴他——「我給你講講我寫的一個組件吧(微笑)」面試
下面開始正題bash
想要作一個狀態管理的組件,首先得了解一下Flutter的刷新流程,在前面寫的 《從源碼看Flutter系列》, 已經對這一過程有所瞭解,下面再簡單介紹一下markdown
setState()
後,將對應的 Element
添加到 BuildOwner
維護的 _dirtyElements
列表中engine
的 frame
回調通知,會觸發 WidgetsBinding
的 drawFrame()
方法,而後會遍歷以前的 _dirtyElements
,根據 Element
在樹中的高度,由上到下調用其 rebuild()
方法進行從新建立或更新Element
的刷新過程當中,會將須要從新layout、paint的 RenderObject
存放在 PipelineOwner
維護的各個列表裏,以後會在 RendererBinding
的 drawFrame()
方法裏對 RenderObject
來一個統一的更新BuildOwner
的 finalizeTree()
來進行統一的銷燬操做了以上就是刷新流程的一個大體介紹。經過這個流程咱們知道,對於須要更新或者銷燬的對象,Flutter的作法就是放入一個列表中進行統一操做,在瞭解到這個事實後,顯然組件狀態也是能夠統一管理的,這也就是後面將要實現的狀態管理組件的核心原理啦。數據結構
在正式介紹狀態管理組件以前,我仍是要先介紹一下 InheritedElement
這個常見嘉賓,Flutter中的全局主題修改等都是基於這個對象的,它對應的 Widget
是 InheritedWidget
,經過使用 InheritedWidget
,咱們也能夠作到跨組件通訊。不過我我的總以爲它的使用方式不太美觀,因此幾乎不多用到。ide
如今很是受歡迎的 provider
庫與以前的 scope_model
,都是基於 InheritedElement
來實現的,可是在使用 provider
的過程當中會遇到這樣一個問題:post
當你在 PageC
經過 Provider.of<ModelB>(context)
來獲取 PageB
對應的 Model
時,是會報錯的,由於獲取到的對象爲null。ui
致使報錯其實涉及到兩個緣由,分別與 InheritedElement
和頁面棧相關,下面就來簡單的說明一下。this
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,點擊體驗
裸辭期間總共開源了兩個組件:
同時,最後聲明一下:落魄小哥,在線求職
有好的內推機會請務必不要放過我,個人聯繫方式就在上面的博客地址中