不到150行代碼,寫一個簡單的Flutter狀態管理組件

前情提要

大概是四月份左右,裸辭了一波。以後就一直在打遊戲、複習、面試中循環度日,到如今尚未一個特別滿意的結果。git

感受本身開始往佛系的方向發展了,難道這就是大起大落後的大徹大悟嗎?github

上面的話就權當開個玩笑,本篇文章的原由是在某次面試中,一位面試官問我Flutter裏跨組件通訊有哪些方式,我說的其中一種就是作一個統一管理,這樣全局獲取後就能夠跨組件通訊了,不過面試官沒有給到一個正面的反饋,因此我就打算作一個這樣的狀態管理組件出來。若是下次再有人問我這個問題,我就會告訴他——「我給你講講我寫的一個組件吧(微笑)」面試

下面開始正題bash

Flutter的刷新流程

想要作一個狀態管理的組件,首先得了解一下Flutter的刷新流程,在前面寫的 《從源碼看Flutter系列》, 已經對這一過程有所瞭解,下面再簡單介紹一下markdown

  • 調用 setState() 後,將對應的 Element 添加到 BuildOwner 維護的 _dirtyElements 列表中
  • 等待 engineframe 回調通知,會觸發 WidgetsBindingdrawFrame() 方法,而後會遍歷以前的 _dirtyElements ,根據 Element 在樹中的高度,由上到下調用其 rebuild() 方法進行從新建立或更新
  • Element 的刷新過程當中,會將須要從新layout、paint的 RenderObject 存放在 PipelineOwner 維護的各個列表裏,以後會在 RendererBindingdrawFrame() 方法裏對 RenderObject 來一個統一的更新
  • 刷新結束後,就是經過 BuildOwnerfinalizeTree() 來進行統一的銷燬操做了

以上就是刷新流程的一個大體介紹。經過這個流程咱們知道,對於須要更新或者銷燬的對象,Flutter的作法就是放入一個列表中進行統一操做,在瞭解到這個事實後,顯然組件狀態也是能夠統一管理的,這也就是後面將要實現的狀態管理組件的核心原理啦。數據結構

InheritedElement與刷新

在正式介紹狀態管理組件以前,我仍是要先介紹一下 InheritedElement 這個常見嘉賓,Flutter中的全局主題修改等都是基於這個對象的,它對應的 WidgetInheritedWidget,經過使用 InheritedWidget,咱們也能夠作到跨組件通訊。不過我我的總以爲它的使用方式不太美觀,因此幾乎不多用到。ide

如今很是受歡迎的 provider 庫與以前的 scope_model,都是基於 InheritedElement 來實現的,可是在使用 provider 的過程當中會遇到這樣一個問題:post

當你在 PageC 經過 Provider.of<ModelB>(context) 來獲取 PageB 對應的 Model 時,是會報錯的,由於獲取到的對象爲null。ui

致使報錯其實涉及到兩個緣由,分別與 InheritedElement 和頁面棧相關,下面就來簡單的說明一下。this

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博客)

同時,最後聲明一下:落魄小哥,在線求職

有好的內推機會請務必不要放過我,個人聯繫方式就在上面的博客地址中

image
相關文章
相關標籤/搜索