Flutter狀態管理 - 初探與總結

Cryptolalia發表於2019-05-13

最近由於Flutter的大火,加上部門可能會開始嘗試在客戶端內落地Flutter的專案,因此最近稍微研究了一下Flutter的一些業務技術。

正好最近看了很多關於Flutter狀態管理的文章,結合我自己對各個方案的一些想法以及大佬們的一些想法,對各個方案進行了一下總結。

狀態管理

flutter的狀態管理分為兩種:區域性狀態和全域性狀態。

區域性狀態:Flutter原生提供了InheritWidget控制元件來實現區域性狀態的控制。當InheritedWidget發生變化時,它的子樹中所有依賴它資料的Widget都會進行rebuild。典型的應用場景有:國際化文案、夜間模式等。

全域性狀態:Flutter沒有提供原生的全域性狀態管理,基本上是需要依賴第三方庫來實現。雖然在根控制元件上使用InheritedWidget也可以實現,不過感覺有點trick.....和React在根節點上使用state有異曲同工之處,會帶來同樣的問題,比如狀態傳遞過深等。

InheritedWidget

優點:

  1. 自動訂閱

InheritedWidget內部會維護一個Widget的Map,當子Widget呼叫Context#inheritFromWidgetOfExactType時就會自動將子Widget存入Map中,並且將InheritedWidget返回給子Widget。

  1. 自動通知

InheritedWidget重建後悔自動觸發InheritElement的Update方法。

缺點:

  1. 無法分離檢視邏輯和業務邏輯。
  2. 無法定向通知/指向性通知。

InheritedWidget不會區分Widget是否需要更新的問題,每次更新都會通知所有的子Widget。因此需要配合StreamBuilder來解決問題。

StreamBuilder是Flutter封裝好的監聽Stream資料變化的Widget,本質上是一個StatefulWidget,內部通過Stream.listen()來監聽傳入的stream的變化,當監聽到有變化時就呼叫setState()方法來更新Widget。

關於stream的介紹的文章到處都有,別人寫的也很詳細,這裡就不再贅述了。

有了StreamBuilder,我們可以在子Widget上通過StreamBuilder來監聽InheritedWidget中的Stream的資料變化,然後判斷是否需要更新當前的子Widget,這樣就完成了資料的定向通知。

ScopedModel

倉庫地址:pub.dartlang.org/packages/sc…

寫法上有點類似目前React比較火的@rematch狀態管理庫,在每個方法中更改完Model資料之後,只需要呼叫一次notifyListeners()就可以更新全部的狀態了。

class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    // First, increment the counter
    _counter++;
    
    // Then notify all the listeners.
    notifyListeners();
  }
}
複製程式碼

在入口處也需要將根元件抱在ScopedModel中,這樣就可以正常工作了。

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // First, create a `ScopedModel` widget. This will provide 
    // the `model` to the children that request it. 
    return new ScopedModel<CounterModel>(
      model: new CounterModel(),
      child: new Column(children: [
        // Create a ScopedModelDescendant. This widget will get the
        // CounterModel from the nearest ScopedModel<CounterModel>. 
        // It will hand that model to our builder method, and rebuild 
        // any time the CounterModel changes (i.e. after we 
        // `notifyListeners` in the Model). 
        new ScopedModelDescendant<CounterModel>(
          builder: (context, child, model) => new Text('${model.counter}'),
        ),
        new Text("Another widget that doesn't depend on the CounterModel")
      ])
    );
  }
}
複製程式碼

(此處程式碼抄自官方Demo)

優點:

  1. 自動訂閱
  2. 自動通知
  3. 簡單易用,對前端開發者來說學習成本幾乎為零

缺點:

  1. 無法分離檢視邏輯和業務邏輯
  2. 無法定向通知/指向性通知

ScopedModel其實只是將InheritedWidget簡單的封裝了一下,因此它繼承了InheritedWidget應有的優點和缺點。

Redux

Redux 是 React 中最流行的狀態管理工具(之一)。Redux儲存了全域性唯一的狀態書,在業務中通過觸發action改變狀態,當狀態改變時,檢視控制元件也隨之更新。Redux解決了狀態傳遞過深的問題,但是因為Dart和js的區別還是很大的,總感覺redux在flutter寫起來並不是很舒服...

Redux將資料和檢視分離,由資料驅動檢視渲染,解決了ScopedModel的檢視和業務分離的問題。

Flutter狀態管理 - 初探與總結

  • Store是一個Model類,內部儲存了一個state。
  • StoreProvider是一個InheritedWidget,內部儲存了一個Store。(資料中心)
  • StoreConnector提供了一個StoreStreamListener,本質上是一個StreamBuilder,它內部有一個Stream<ViewModel>,這個Stream是由Store中的changeController這個SteamController的Stream呼叫map方法轉化來的。。
  • StoreStreamListener通過監聽自己的Stream來完成檢視的重建。

簡單來說就是StoreConnector負責將資料中心的Stream<State>變成Stream<ViewModel>,然後StoreStreamListener負責監聽Stream<ViewModel>的變化來更新子Widget。

流程:

Flutter狀態管理 - 初探與總結

  1. View層發出Action
  2. Store中的dispatch將這個action轉化成Stream<State>,並新增到changeControllerStream<State>中等待執行。
  3. StoreStreamListener監聽到有新的Stream<State>流入,就把流入的State按照業務方事先約定好的covert方法轉化成ViewModel,然後將這個ViewModel傳入到Stream<ViewModel>中。
  4. View層監聽到有新的Stream流入,rebuild整個View。

優點:

  1. 自動訂閱
  2. 自動通知
  3. 可以定向通知
  4. 檢視和業務邏輯分離

本來看了網上很多文章,感覺在Flutter中使用Redux似乎是一個可行的方案,不過看到公司內部有大佬對Flutter中的Redux的分析文章,確實存在的問題還很多。

因為 Dart 與 JavaScript 直接的區別,Redux 在 Flutter 中使用有許多難以解決的問題

比如通過對比Store建構函式和combineReducers函式:

// dart
Store(
  this.reducer, {
  State initialState,
  List<Middleware<State>> middleware = const [],
  bool syncStream: false,
);
    
Reducer<State> combineReducers<State>(Iterable<Reducer<State>> reducers);
複製程式碼
// ts
function createStore(reducer: Reducer);
function combineReducers(reducers: ReducersMapObject): Reducer;
複製程式碼

結合函式原型和平時使用的情況,可以看出二者之間的差異。js中combineReducers傳入的值是一個Reducer的對映結構,在函式執行的過程中,每個部分的隱含狀態樹被整合到一起,成為一顆完整的樹。

Dart中沒有這樣的動態結構,只能在建立Store的時候顯示傳入所有的初始狀態樹,這有悖於"解耦"的理念。如果將不同部分的狀態存入不同的store的話,這些狀態之間的交換又會變得十分困難,這與Redux本身的設計理念不符。

並且在Dart中,immutable資料的建立也十分麻煩,Dart中沒有js中物件解構的"..."運算子。

const newState = {
    ...oldState,
    count: count + 1
};
複製程式碼

基於以上種種原因,雖然開始比較看好Redux,最後我還是放棄了使用Redux...

BloC

BloC的核心思想是資料與檢視分離,由資料變化驅動試圖渲染。(沒錯和redux一模一樣)

從某種意義上講Redux可以看做是一種特殊的BloC。

介紹文件已經有大佬在掘金發表過了:juejin.im/post/5bb6f3… 我也是看這篇文章學習的BloC的相關知識。

BloC和Redux的區別在於:redux有一個資料中心store才存放所有的資料,當資料改變時由資料中心呼叫covert方法將state轉換成對應的ViewModel,然後通知子Widget進行修改。

Flutter狀態管理 - 初探與總結

而在BloC中則沒有store的概念,只有一個StreamController,但是這個Controller並不存放資料,只是處理資料的,並且BloC沒有convert方法,Viwe會直接將State轉換成ViewModel。

Flutter狀態管理 - 初探與總結

在Redux的優點的基礎上,BloC將業務分離地更加徹底,並且解決了Redux難以分離各個部分狀態的痛點,一個應用程式可以有多個資料來源,並且可以通過流操作對其進行加工組合,具有較強的擴充套件性,加上Dart原生支援Stream類,書寫起來也比較方便。

在業務中使用BloC方案時,不需要我們重新用Stream實現這一套方案,可以直接使用flutter_bloc庫即可:github.com/felangel/bl…

reBloC

地址:github.com/RedBrogdon/… rebloc是redux+bloc的一個實現方案。

Rebloc is an attempt to smoosh together two popular Flutter state management approaches: Redux and BLoC. It defines a Redux-y single direction data flow that involves actions, middleware, reducers, and a store. Rather than using functional programming techniques to compose reducers and middleware from parts and wire everything up, however, it uses BLoCs.

The store defines a dispatch stream that accepts new actions and produces state objects in response. In between, BLoCs are wired into the stream to function as middleware, reducers, and afterware. Afterware is essentially a second chance for Blocs to perform middleware-like tasks after the reducers have had their chance to update the app state.

官方的說明是想要結合redux的資料流(解決指向性通知)方案以及bloc的響應式程式設計的更少的編碼量。 但是感覺這個方案既擁有redux的複雜性,又引入了bloc的閉環stream流,最終導致整個方案更加複雜了。

fish_redux

fish_redux是阿里鹹魚開源的一套flutter設計方案,介紹:zhuanlan.zhihu.com/p/55062930 也是基於redux進行了一下改良封裝,多了幾個新的概念:Adapter、Component。

redux本身只提供一種全域性狀態管理方案,並不關心具體業務。fish_redux是針對業務方對redux又進行了一次使用層面的改良。

每個元件(Component)需要定義一個資料(Struct)和一個Reducer。同時元件之間的依賴關係解決了集中和分治的矛盾。

Component Component的概念有點類似我們rematch中的model,含有View、Effect、Reducer三部分。 View負責展示 Effect負責非state修改的函式 Reducer負責修改state的函式

Adapter 由於Flutter中ListView的高頻使用,fish_redux對ListView做了效能優化,Adapter由此出現。

它的目標是解決 Component 模型在 flutter-ListView 的場景下的 3 個問題:

  1. 將一個"Big-Cell"放在 Component 裡,無法享受 ListView 程式碼的效能優化。
  2. Component 無法區分 appear|disappear 和 init|dispose 。
  3. Effect 的生命週期和 View 的耦合,在 ListView 的場景下不符合直觀的預期。 概括的講,我們想要一個邏輯上的 ScrollView,效能上的 ListView ,這樣的一種區域性展示和功能封裝的抽象。 做出這樣獨立一層的抽象是, 我們看實際的效果, 我們對頁面不使用框架,使用框架 Component,使用框架 Component+Adapter 的效能基線對比

fish_redux目錄結構:

  • page
  • --sample_page
  • ---- action.dart
  • ---- page.dart
  • ---- view.dart
  • ---- effect.dart
  • ---- reducer.dart
  • ---- state.dart
  • components
  • --sample_component
  • ---- action.dart
  • ---- component.dart
  • ---- view.dart
  • ---- effect.dart
  • ---- reducer.dart
  • ---- state.dart

優點:

  1. 資料集中管理,框架自動完成reducer合併。
  2. 元件分治管理,元件之間以及和容器之間互相隔離。
  3. View、Reducer、Effect隔離。易於編寫複用。
  4. 宣告式配置組裝。
  5. 良好的擴充套件性。

個人感覺fish_redux的設計適用於複雜的業務場景,加上覆雜的目錄結構以及相關概念,不太適合普通的資料不太複雜的業務。

Mobx

地址:pub.dev/packages/mo…

與BloC類似,MobX也是觀察者模式。但是MobX將所有的更新和訊息推送都隱藏在了getter和setter裡面,因此開發者在使用的時候無需關心訊息傳送和響應的時機,元件會在任何它依賴的物件更新時進行重新渲染。

Dart版本的MobX使用起來和js很像,由於Dart沒有裝飾器,因此MobX使用了mobx_codegen生成部分程式碼代替了這部分的工作:github.com/mobxjs/mobx…

import 'package:mobx/mobx.dart';

// Include generated file
part 'todos.g.dart';

// This is the class used by rest of your codebase
class Todo = TodoBase with _$Todo;

// The store-class
abstract class TodoBase implements Store {
  TodoBase(this.description);

  @observable
  String description = '';

  @observable
  bool done = false;
}
複製程式碼

(以上程式碼抄自官方Demo)

以上建立了一個包含響應式狀態以及對應方法的類。使用mobx_codegen生成的_$Todo中繼承了descriptiondone屬性,並且給他們加上了額外的操作,使得狀態可以被捕獲。

這裡是一個官方Counter的Demo:

// package:mobx_examples/counter/counter.dart
import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter; // 這裡是mobx_codegen生成的

abstract class CounterBase implements Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}
複製程式碼
// counter_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx_examples/counter/counter.dart';

class CounterExample extends StatefulWidget {
  const CounterExample();

  @override
  CounterExampleState createState() => CounterExampleState();
}

class CounterExampleState extends State<CounterExample> {
  final Counter counter = Counter();

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Observer(builder: (_) => Text('${counter.value}')),
      RaisedButton(
        child: Text('inc'),
        onPressed: counter.increment,
      )
    ]);
  }
}
複製程式碼

當按鈕被點選時,increment方法就會被觸發,從而改變value值,然後上報變化。Observer收到更新訊息後會重新渲染元件。根據 MobX 的原理,可以發現上面提出的問題都可以被解決。元件的更新與資料引用是否改變完全沒有關係,它只關心使用的值是否發生了改變,因此完全不需要考慮 immutable 的問題。

MobX 還有一個其它方案較難實現的優點,它可以以很低的代價建立很多相同結構的狀態以及相應的操作。例如在維護一個列表時,可以在每一個列表項的元件中建立一個 MobX 的物件,然後讓裡面的子元件響應這個物件的更新操作。以上面的元件為例,無論建立多少個 CounterExample 物件,都會有相應的 Counter 在裡面,不需要把這些狀態以一種不自然的方式組合到一起。另外如果有其它型別的元件也有類似的狀態和操作,也可以使用這個類,減少重複的開發。這正是 React Hooks 想要解決的問題,在此之前,原生的 React 沒有比較合適的方法處理這種場景(在 Dart 中也可以使用 mixin 得到類似的效果)。Redux 需要處理狀態陣列或者狀態的字典,這是一個比較複雜的操作,尤其是在 Dart 環境下。BloC 的方式依賴於 InheritWidget 獲取上下文,如果有多個相同型別的狀態物件在元件樹中容易引發衝突,需要使用額外的方法解決這個問題。

總結

與 React 類似,Flutter 可以使用 setState 管理元件區域性的狀態,但是很難僅僅使用 setState 來管理整個複雜應用。

在上面提到的狀態管理方案中,感覺比較好用的只有BloC和MobX,如果習慣vue和MobX的同學建議直接使用MobX,MobX的資料響應幾乎透明,開發者可以更加自由地組織自己想要的狀態。

Flutter目前感覺還太年輕,狀態管理方案各家也都在探索,像鹹魚自己出的fish_redux,社群內還沒有一個比較完美的狀態管理方案,根據自己的業務選擇合適的狀態管理方案應該是最好的答案了。

(如果文章中有Stream打成了Steam請忽略QAQ)

相關文章