最近由於Flutter的大火,加上部門可能會開始嘗試在客戶端內落地Flutter的專案,因此最近稍微研究了一下Flutter的一些業務技術。
正好最近看了很多關於Flutter狀態管理的文章,結合我自己對各個方案的一些想法以及大佬們的一些想法,對各個方案進行了一下總結。
狀態管理
flutter的狀態管理分為兩種:區域性狀態和全域性狀態。
區域性狀態:Flutter原生提供了InheritWidget控制元件來實現區域性狀態的控制。當InheritedWidget發生變化時,它的子樹中所有依賴它資料的Widget都會進行rebuild。典型的應用場景有:國際化文案、夜間模式等。
全域性狀態:Flutter沒有提供原生的全域性狀態管理,基本上是需要依賴第三方庫來實現。雖然在根控制元件上使用InheritedWidget也可以實現,不過感覺有點trick.....和React在根節點上使用state有異曲同工之處,會帶來同樣的問題,比如狀態傳遞過深等。
InheritedWidget
優點:
- 自動訂閱
InheritedWidget內部會維護一個Widget的Map,當子Widget呼叫Context#inheritFromWidgetOfExactType時就會自動將子Widget存入Map中,並且將InheritedWidget返回給子Widget。
- 自動通知
InheritedWidget重建後悔自動觸發InheritElement的Update方法。
缺點:
- 無法分離檢視邏輯和業務邏輯。
- 無法定向通知/指向性通知。
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)
優點:
- 自動訂閱
- 自動通知
- 簡單易用,對前端開發者來說學習成本幾乎為零
缺點:
- 無法分離檢視邏輯和業務邏輯
- 無法定向通知/指向性通知
ScopedModel
其實只是將InheritedWidget簡單的封裝了一下,因此它繼承了InheritedWidget應有的優點和缺點。
Redux
Redux 是 React 中最流行的狀態管理工具(之一)。Redux儲存了全域性唯一的狀態書,在業務中通過觸發action改變狀態,當狀態改變時,檢視控制元件也隨之更新。Redux解決了狀態傳遞過深的問題,但是因為Dart和js的區別還是很大的,總感覺redux在flutter寫起來並不是很舒服...
Redux將資料和檢視分離,由資料驅動檢視渲染,解決了ScopedModel的檢視和業務分離的問題。
- 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。
流程:
- View層發出Action
- Store中的dispatch將這個action轉化成
Stream<State>
,並新增到changeController
的Stream<State>
中等待執行。 - StoreStreamListener監聽到有新的
Stream<State>
流入,就把流入的State按照業務方事先約定好的covert方法轉化成ViewModel,然後將這個ViewModel傳入到Stream<ViewModel>
中。 - View層監聽到有新的Stream流入,rebuild整個View。
優點:
- 自動訂閱
- 自動通知
- 可以定向通知
- 檢視和業務邏輯分離
本來看了網上很多文章,感覺在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進行修改。
而在BloC中則沒有store的概念,只有一個StreamController,但是這個Controller並不存放資料,只是處理資料的,並且BloC沒有convert方法,Viwe會直接將State轉換成ViewModel。
在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 個問題:
- 將一個"Big-Cell"放在 Component 裡,無法享受 ListView 程式碼的效能優化。
- Component 無法區分 appear|disappear 和 init|dispose 。
- 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
優點:
- 資料集中管理,框架自動完成reducer合併。
- 元件分治管理,元件之間以及和容器之間互相隔離。
- View、Reducer、Effect隔離。易於編寫複用。
- 宣告式配置組裝。
- 良好的擴充套件性。
個人感覺fish_redux的設計適用於複雜的業務場景,加上覆雜的目錄結構以及相關概念,不太適合普通的資料不太複雜的業務。
Mobx
與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
中繼承了description
和done
屬性,並且給他們加上了額外的操作,使得狀態可以被捕獲。
這裡是一個官方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)