不到150行程式碼,寫一個簡單的Flutter狀態管理元件

安卓小哥發表於2020-05-26

前情提要

大概是四月份左右,裸辭了一波。之後就一直在打遊戲、複習、面試中迴圈度日,到現在還沒有一個特別滿意的結果。

感覺自己開始往佛系的方向發展了,難道這就是大起大落後的大徹大悟嗎?

上面的話就權當開個玩笑,本篇文章的起因是在某次面試中,一位面試官問我Flutter裡跨元件通訊有哪些方式,我說的其中一種就是做一個統一管理,這樣全域性獲取後就可以跨元件通訊了,不過面試官沒有給到一個正面的反饋,所以我就打算做一個這樣的狀態管理元件出來。如果下次再有人問我這個問題,我就會告訴他——“我給你講講我寫的一個元件吧(微笑)”

下面開始正題

Flutter的重新整理流程

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

  • 呼叫 setState() 後,將對應的 Element 新增到 BuildOwner 維護的 _dirtyElements 列表中
  • 等待 engineframe 回撥通知,會觸發 WidgetsBindingdrawFrame() 方法,然後會遍歷之前的 _dirtyElements ,根據 Element 在樹中的高度,由上到下呼叫其 rebuild() 方法進行重新建立或更新
  • Element 的重新整理過程中,會將需要重新layout、paint的 RenderObject 存放在 PipelineOwner 維護的各個列表裡,之後會在 RendererBindingdrawFrame() 方法裡對 RenderObject 來一個統一的更新
  • 重新整理結束後,就是通過 BuildOwnerfinalizeTree() 來進行統一的銷燬操作了

以上就是重新整理流程的一個大致介紹。通過這個流程我們知道,對於需要更新或者銷燬的物件,Flutter的做法就是放入一個列表中進行統一操作,在瞭解到這個事實後,顯然元件狀態也是可以統一管理的,這也就是後面將要實現的狀態管理元件的核心原理啦。

InheritedElement與重新整理

在正式介紹狀態管理元件之前,我還是要先介紹一下 InheritedElement 這個常見嘉賓,Flutter中的全域性主題修改等都是基於這個物件的,它對應的 WidgetInheritedWidget,通過使用 InheritedWidget,我們也可以做到跨元件通訊。不過我個人總覺得它的使用方式不太美觀,所以幾乎很少用到。

現在非常受歡迎的 provider 庫與之前的 scope_model,都是基於 InheritedElement 來實現的,但是在使用 provider 的過程中會遇到這樣一個問題:

不到150行程式碼,寫一個簡單的Flutter狀態管理元件

當你在 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 列表裡,也就是說所有的頁面在資料結構上實際是平級的關係,下面用一個簡單的圖形表示一下

不到150行程式碼,寫一個簡單的Flutter狀態管理元件

因為 InheritedElement 的查詢就是通過父節點向上遍歷,直到找到指定的物件為止,否則返回null,而這裡由於 PageC 與 PageB 是平級的關係,顯然 PageC 無法找到 PageB 對應的資料(實際上是對應的Element為平級,這裡做了簡化)

這也就是使用 provider 會遇到這樣問題的原因,當然解決辦法也很簡單,就是將 Model 都放入 GlobalModel 中,通過 GlobalModel 獲取即可

上面介紹完的這些對於理解狀態管理有一定的幫助,下面就開始正式介紹我是如何實現狀態管理元件的

實現狀態管理元件

實現的思路非常簡單,就是通過維護一個 HashMap 物件,將各個頁面對應的 Model 放入其中,獲取的時候通過這個 HashMap 獲取即可。

不過可能會遇到下面這種場景:

不到150行程式碼,寫一個簡單的Flutter狀態管理元件

當需要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,點選體驗

最後

裸辭期間總共開源了兩個元件:

同時,最後宣告一下:落魄小哥,線上求職

有好的內推機會請務必不要放過我,我的聯絡方式就在上面的部落格地址中

image

相關文章