flutter的狀態管理方案不少,這裏對相關知識作一下梳理。html
Flutter將組件分爲StatefulWidget,StatelessWidget,有狀態的組件一般繼承自StatefulWidget,經過State來管理自身狀態。react
當業務比較簡單時,基於StatefulWidget就能夠管理好整個應用的狀態了。git
可是當業務複雜起來時,會存在一些問題:github
如圖,有兩個根節點須要共享某個狀態,那麼就須要把這個狀態存到它們共同的父節點,而後逐級傳遞下來,當狀態改變時,共同父節點之下的整個子樹都會rebuild,而其中大部分組件的rebuild都是多餘的,致使性能變差。編程
另外,這種方式可能會使父組件的state變得臃腫,有些數據可能並不適合放到它的state裏可是爲了共享只能放那兒。redux
所以,咱們須要更進一步的手段進行狀態管理。設計模式
多說兩句,有部分客戶端開發在接觸Flutter時並非很接受類React的這種範式,會想着把狀態丟在某個管理器單例中,建立頁面/Widget時去取用,若是狀態改變時須要頁面刷新,就拋個通知或者相似的方式讓組件去更新。安全
在Flutter的世界裏,把某個/某些全局狀態搞成單例不是不能夠,但一般仍傾向於保留響應式的優勢(Flutter自己不算徹底的響應式,由於setState是顯式的,但至少有部分響應式的特色),組件在使用狀態時維護監聽關係,狀態改變時通知訂閱者。這種通知方式是像setState同樣天然甚至是沒有這種顯式地發出通知步驟的,組件在讀狀態時就產生了訂閱關係,而不是顯式地訂閱通知。總而言之,前面提到的想法太命令式了。性能優化
官方提供的共享數據基本方案。架構
InheritedWidget是一種特殊的功能性組件,它提供了一種將狀態從上向下傳遞的方式。
好比Material組件中Theme的管理就使用了這種方式,在一個MaterialApp中,咱們能夠在build任意widget時使用ThemeData data = Theme.of(context)
來獲取當前主題數據,同時也會使當前Widget依賴了Theme(準確地說是_InheritedTheme
這一組件),當主題變化時,全部依賴它的Widget都會被更新。這就解決了直接傳遞State致使多餘的組件更新的問題。
看個demo,定義一個ShareDataWidget繼承自InheritedWidget:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data;
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
@override
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
複製代碼
在父組件中:
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget( //使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),//子widget中依賴ShareDataWidget
),
RaisedButton(
child: Text("Increment"),
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
複製代碼
子組件獲取InheritedWidget中的數據:
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
複製代碼
能夠看到,數據仍是父組件的state管理的,InheritedWidget只是對數據包裝了一下,提供了一個通道供子組件取數據。
這裏比較關鍵的一點是inheritFromWidgetOfExactType的實現,這決定了子組件尋找共享的Model的性能,看一下實現:
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
複製代碼
_inheritedWidgets是個Map,dart中的Map默認是LinkedHashMap,這裏的查找操做是O(1)的。所以性能是有保障的。
InheritedWidget缺點仍是比較明顯,一方面直接使用時代碼上比較囉嗦,另外一方面只提供了從上到下的數據傳遞,子組件想要改數據還須要使用Notification機制,就更重了。
因爲InheritedWidget性能好但使用不便,社區在此基礎上進行了不少封裝。
ScopedModel是早期Flutter社區封裝的比較成功的狀態管理組件。它把狀態封裝入Model裏,修改數據的邏輯也在model裏,經過跟InheritedWidget相似的方式把Model傳給子組件,子組件能夠從Model取數據,也能夠直接調用Model的方法修改數據。
不事後來Flutter官方選擇了更優秀且一樣方便的狀態管理框架provider進行推廣,這裏就很少介紹ScopedModel了。
provider目前是官方欽定的應用狀態管理組件。
Pragmatic State Management in Flutter (Google I/O'19)
看個demo
class CounterModel with ChangeNotifier {
int _count = 0;
int get value => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
final counter = CounterModel();
runApp(
ChangeNotifierProvider.value(
value: counter,
child: MaterialApp(
home:FirstScreen()
),
),
);
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _counter = Provider.of<CounterModel>(context);
final textSize = Provider.of<int>(context).toDouble();
return Scaffold(
appBar: AppBar(
title: Text('FirstPage'),
),
body: Center(
child: Text(
'Value: ${_counter.value}',
style: TextStyle(fontSize: textSize),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _counter.increment,
child: Icon(Icons.navigate_next),
),
);
}
}
複製代碼
定義一個CounterModel,經過Provider組件注入組件樹,子組件在build時能夠經過Provider.of<CounterModel>(context)
獲取model,能夠從model中獲取數據,也能夠直接調用model的方法(CounterModel.increment)修改數據。
能夠看到這樣作比InheritedWidget方便太多了。
這裏用到的是ChangeNotifierProvider,跟ScopedModel用起來是基本同樣的,它也是咱們最經常使用的Provider。但它比ScopedModel優秀的地方在於,使用ScopedModel時必須繼承它的Model才能用,所以ScopedModel是有比較強的侵入性的,而Provider就好不少。
除了ChangeNotifierProvider以外,Provider還提供了另外幾種方式進行數據管理:
ChangeNotifier.dispose
釋放資源。通常不用。notifyListeners()
。前面幾個庫,大致上來說,只是提供了基本的數據管理能力,也就是,專心作好本身數據通道的職責,並未對具體實現作過多的限制的話。
相比之下,Redux就重得多了,它不僅是作了個數據通道,還對整個數據層的操做進行了比較強的約束,至關於提供了比較完整的數據層設計模式。場景比較簡單的話,這會顯得代碼很累贅;但項目比較大場景比較複雜的話,這些約束能夠有效阻止代碼的腐爛。
咱們來看一下Redux的核心概念:
Redux有一個Store用於存儲數據,咱們的Model放在Store.state這個屬性中,對數據的get操做,跟前面的框架並無太大區別,當Store中數據變化時也會觸發View的刷新;而對數據的set操做,Redux提出了很高的要求。
Provider框架下的Model默認實際上是個比較重的model(固然你也能夠進一步拆分),數據從model裏出,view須要修改數據時調用model的方法,甚至能夠直接修改model的屬性而後拋個notifyListeners
出來。
這在Redux的模式下是不容許的。Redux的理由是:在這種不受約束的狀況下,可能到處都會去改model,當業務愈來愈複雜的時候,這裏狀態的變化極可能會變得難以追溯:state在何時、出於什麼緣由、如何發生變化變得不受控制。Redux的全部設計的出發點就在這裏:讓state的變化變得可預測。
所以Redux對state的set進行了高度封裝,爲了全部state的修改有據可查,Redux引入了Action的概念,這個有點像客戶端經常使用的Notification,每一個通知有本身的標識。
具體使用上,Action能夠是任意的類型,不須要攜帶數據的話能夠用enum,給每種Action定義一個枚舉;也能夠給每一個Action定義一個class,如:
class SearchLoadingAction {}
class SearchErrorAction {}
class SearchResultAction {
final SearchResult result;
SearchResultAction(this.result);
}
複製代碼
總之,Action應當可以區分類型,而且能夠攜帶數據,一般只會帶些比較簡單的參數。
view層接收到用戶輸入時,產生一個Action經過Store進行分發store.dispatch(Actions.Increment)
所以,咱們須要一個地方處理Action並更新state,這又引入了一個Reducer的概念。其實就是個處理Action更新State的函數。
固然它有一點特別的地方,你能夠把State想成一個字典,每次Reducer改動State時,會對State作一次淺拷貝並修改其中的某個值,Store會存儲新的State,而舊的State中的全部內容都是不變的,這稱爲不可變對象。很直接的好處是,這樣修改的State,只要對比先後兩個State的指針是否一致就知道State是否被修改過了(State中的屬性能夠嵌套不少層,道理是同樣的,逐層比較指針便可)。此外,這種方式約束了數據的修改,提升了數據處理的安全性。而且,能夠很方便地記錄State變化的序列以及觸發其變化的Action,方便調試、追溯數據變化的過程。
總結一下Redux的三大原則,其實上面已經提到了一部分了。
第一大原則是單一數據源。Redux建議整個應用的State被存儲在一棵object tree中,而且這個object tree只存在於惟一一個store中。單Store和多Store一直以來有諸多爭議,Redux認爲集中式的Store更方便管理,也容易調試,而且避免了多個Store間同步數據的問題;而多Store則方便組件化、模塊化的拆分,不一樣業務負責本身的Store也比較符合通常開發習慣。
第二大原則是state是隻讀的。前面已經講得比較清楚了,爲了讓State的變化可預測,所以Redux中state是隻讀的,想要修改必現經過Action、Reducer進行修改。
第三大原則是用純函數執行修改。也就是newState = oldState + Action這種方式,前面Reducer部分也講得很清楚了。
剛剛,阿里宣佈開源Flutter應用框架Fish Redux!
flutter_redux是基本徹底遵循Redux規範的flutter實現。而閒魚在實踐過程當中,在flutter-redux基礎上進行了進一步的封裝。
純flutter應用使用flutter-redux是沒什麼問題的,可是不少應用是像閒魚這樣,在Native應用的基礎上集成Flutter進行混合開發的,這種開發每每是以頁面爲單位的。所以對業務邏輯的分治、可插拔的組件化有比較強的需求,在這種背景下Redux應當如何實踐,fish-redux給出了一個完整的框架。
看一下fish-redux的demo項目
fish-redux提出了Component的概念,能夠理解爲「Redux組件」,Component自己有完整的Redux能力(state/action/reducer),也能夠方便地嵌入全局的Redux體系中。這套設計Fish-Redux自稱可插拔的組件體系、既保留了Redux的原則又提供了業務代碼分治的能力。
fish-redux涉及的新概念很是多,若是要用的話,對Redux應當有比較豐富的經驗,而且,不建議簡單應用使用這麼重的框架。
基於流的響應式狀態管理,由Google在2018的 DartConf首次提出。
毫無疑問,Stream或者更進一步的ReactiveX是純粹的響應式編程,但其實Redux也經過Action、Reducer這樣的方式實現了響應式(或許某種程度上不是特別純粹,對異步的處理可能比起Stream的方式略有不足),所以實際使用時BloC和Redux其實會有必定的類似性。
BLoC表明業務邏輯組件(Business Logic Component),它的思想其實很樸素,基本上就是兩點,第一是把業務邏輯抽離出來封裝成獨立的BLoC組件,這個其實每一個數據流框架都是要這麼作的;第二是使用流的方式構造響應式的數據流。
如上圖所示,BLoC封裝了全部的業務邏輯,經過Stream輸出給Widget,而Widget產生的事件會拋給BLoC,BLoC處理後把數據塞進流的Sink中觸發訂閱者的更新。
BLoC自己只是個設計模式,並無限定具體的實現,目前各方的實現也略有區別。能夠經過InheritedWidget將BLoC注入組件樹由子組件獲取,也能夠把BLoC作成一個單例去獲取;流的部分能夠用原生的Stream或RxDart。在此基礎上還有一些更進一步的封裝,可以減小一些重複代碼。
這裏咱們看一下在flutter_bloc這個庫基礎上的BLoC實現:
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider(
create: (context) => CounterBloc(),
child: CounterPage(),
)
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => BlocProvider.of<CounterBloc>(context)
.add(CounterEvent.increment),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () => BlocProvider.of<CounterBloc>(context)
.add(CounterEvent.decrement),
),
)
],
),
);
}
}
enum CounterEvent { increment, decrement }
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield state - 1;
break;
case CounterEvent.increment:
yield state + 1;
break;
}
}
}
複製代碼
子組件經過BlocProvider.of
拿到BLoC實例,經過Action的方式分發事件給BLoC,組件經過BlocBuilder
獲取數據進行構建。
Flutter目前狀態管理這塊呈現出一個百花齊放的狀態,簡單總結一下。
僅使用state自己,在很小的應用上還能搞搞,中型應用是扛不住的,性能上缺少有效的優化手段,數據層架構上也很難維護。
InheritedWidget自己用起來太麻煩,更多地是做爲一種基礎能力給上層框架,而不是直接用。
ScopedModel和Provider差很少是同類產品,能力上Provider更豐富一些而且有官方背書,通常的中小型應用都傾向於Provider一些。
Redux算是更復雜一點的數據層方案了,跟BLoC能夠比較一下,這二者在響應式的路上比原始的State更進一步(或許BLoC更純粹一些),Redux的優勢是狀態的變化比較可控,Action/Reducer的設計讓狀態的變化有理有據。BLoC的優勢是對異步的處理更好一些。
最後說一下fish-redux,這實際上是以Redux爲核心定製的應用框架,而且不少理念是爲混合應用而不是純Flutter應用考慮的,雖然github上star不少...可是感受實際上適合用它的應用並很少,中小型的用不上,大型的,仍是混合應用,人家每每也有本身定製化的考量,或許更樂意從redux的基礎上進行定製而不是拿fish-redux去改。
橫向對比大概就是這些,而後說說性能優化。
除了BLoC外,不管哪一種方式將狀態注入widget tree,想要獲取狀態,大致上都是兩種方式
,一種是在build方法中使用Provider.of<Model>(context)
獲取Model,一般這會使得當前組件訂閱Model,當Model發生變化時,引發當前組件rebuild;另外一種是使用一個Consumer類的組件獲取狀態並傳遞給子組件,如:
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
複製代碼
一般來說,Comsumer類組件由於能夠用builder方法來生成子組件,能作的優化會多不少。好比這個來自於Provider的例子,能夠看到嵌套層級是Foo -> Bar -> Baz,使用Consumer封裝後,其實只有Bar刷新了,上層和下層的Foo和Baz都沒有rebuild,而若是使用``Provider.of`方式,子組件是必然會所有rebuild的。
此外,Provider還提供了一種Selector訂閱的方式:
Selector<List, int>(
selector: (_, list) => list.length,
builder: (_, length, __) {
return Text('$length');
}
);
複製代碼
此時只有selector的值產生變化時纔會觸發刷新,在這裏也就是list.length。
flutter-redux也提供了相似方案:
StoreConnector<AppState, AppViewModel>(
distinct: true,
builder: (context, vm) {
return App(vm.isLogin, vm.key);
},
converter: (store) => AppViewModel.fromStore(store))
複製代碼
使用StoreConnector注入狀態,不過要顯式指定distinct: true才能依靠vm進行狀態變化的過濾,一個使用Redux的中型應用,這應當是必須作的優化,畢竟,Redux秉承單一Store原則,牽一髮而動全身。目前看起來框架做者甚至沒有在readme裏提這回事兒,感受不少人用flutter-redux會有嚴重的性能問題。
Provider之流自己是推薦多個Model的,大部分widget只會依賴本身相關的model,潛在的性能風險要小不少,更況且Provider提供的性能優化能力還更加完善。
綜上,我推薦中小型應用使用Provider,功能豐富,使用簡單,性能優化能力也提供得比較完整。