最近因爲Flutter的大火,加上部門可能會開始嘗試在客戶端內落地Flutter的項目,所以最近稍微研究了一下Flutter的一些業務技術。前端
正好最近看了不少關於Flutter狀態管理的文章,結合我本身對各個方案的一些想法以及大佬們的一些想法,對各個方案進行了一下總結。vue
flutter的狀態管理分爲兩種:局部狀態和全局狀態。git
局部狀態:Flutter原生提供了InheritWidget控件來實現局部狀態的控制。當InheritedWidget發生變化時,它的子樹中全部依賴它數據的Widget都會進行rebuild。典型的應用場景有:國際化文案、夜間模式等。github
全局狀態:Flutter沒有提供原生的全局狀態管理,基本上是須要依賴第三方庫來實現。雖然在根控件上使用InheritedWidget也能夠實現,不過感受有點trick.....和React在根節點上使用state有殊途同歸之處,會帶來一樣的問題,好比狀態傳遞過深等。編程
優勢:redux
InheritedWidget內部會維護一個Widget的Map,當子Widget調用Context#inheritFromWidgetOfExactType時就會自動將子Widget存入Map中,而且將InheritedWidget返回給子Widget。數組
InheritedWidget重建後悔自動觸發InheritElement的Update方法。性能優化
缺點:app
InheritedWidget不會區分Widget是否須要更新的問題,每次更新都會通知全部的子Widget。所以須要配合StreamBuilder來解決問題。框架
StreamBuilder是Flutter封裝好的監聽Stream數據變化的Widget,本質上是一個StatefulWidget
,內部經過Stream.listen()
來監聽傳入的stream
的變化,當監聽到有變化時就調用setState()
方法來更新Widget。
關於stream
的介紹的文章處處都有,別人寫的也很詳細,這裏就再也不贅述了。
有了StreamBuilder
,咱們能夠在子Widget上經過StreamBuilder
來監聽InheritedWidget中的Stream
的數據變化,而後判斷是否須要更新當前的子Widget,這樣就完成了數據的定向通知。
倉庫地址: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 是 React 中最流行的狀態管理工具(之一)。Redux保存了全局惟一的狀態書,在業務中經過觸發action改變狀態,當狀態改變時,視圖控件也隨之更新。Redux解決了狀態傳遞過深的問題,可是由於Dart和js的區別仍是很大的,總感受redux在flutter寫起來並非很舒服...
Redux將數據和視圖分離,由數據驅動視圖渲染,解決了ScopedModel的視圖和業務分離的問題。
簡單來講就是StoreConnector
負責將數據中心的Stream<State>
變成Stream<ViewModel>
,而後StoreStreamListener負責監聽Stream<ViewModel>
的變化來更新子Widget。
流程:
Stream<State>
,並添加到changeController
的Stream<State>
中等待執行。Stream<State>
流入,就把流入的State按照業務方事先約定好的covert方法轉化成ViewModel,而後將這個ViewModel傳入到Stream<ViewModel>
中。優勢:
原本看了網上不少文章,感受在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的核心思想是數據與視圖分離,由數據變化驅動試圖渲染。(沒錯和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…
地址: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是阿里鹹魚開源的一套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 個問題:
fish_redux目錄結構:
優勢:
我的感受fish_redux的設計適用於複雜的業務場景,加上覆雜的目錄結構以及相關概念,不太適合普通的數據不太複雜的業務。
與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)