Flutter狀態管理

Flutter狀態管理

flutter的狀態管理方案不少,這裏對相關知識作一下梳理。html

傳遞State

Flutter將組件分爲StatefulWidget,StatelessWidget,有狀態的組件一般繼承自StatefulWidget,經過State來管理自身狀態。react

當業務比較簡單時,基於StatefulWidget就能夠管理好整個應用的狀態了。git

可是當業務複雜起來時,會存在一些問題:github

Build-reactive-mobile-apps-with-Flutter--Google-I-2FO--18--0001

如圖,有兩個根節點須要共享某個狀態,那麼就須要把這個狀態存到它們共同的父節點,而後逐級傳遞下來,當狀態改變時,共同父節點之下的整個子樹都會rebuild,而其中大部分組件的rebuild都是多餘的,致使性能變差。編程

另外,這種方式可能會使父組件的state變得臃腫,有些數據可能並不適合放到它的state裏可是爲了共享只能放那兒。redux

所以,咱們須要更進一步的手段進行狀態管理。設計模式

多說兩句,有部分客戶端開發在接觸Flutter時並非很接受類React的這種範式,會想着把狀態丟在某個管理器單例中,建立頁面/Widget時去取用,若是狀態改變時須要頁面刷新,就拋個通知或者相似的方式讓組件去更新。安全

在Flutter的世界裏,把某個/某些全局狀態搞成單例不是不能夠,但一般仍傾向於保留響應式的優勢(Flutter自己不算徹底的響應式,由於setState是顯式的,但至少有部分響應式的特色),組件在使用狀態時維護監聽關係,狀態改變時通知訂閱者。這種通知方式是像setState同樣天然甚至是沒有這種顯式地發出通知步驟的,組件在讀狀態時就產生了訂閱關係,而不是顯式地訂閱通知。總而言之,前面提到的想法太命令式了。性能優化

InheritedWidget

官方提供的共享數據基本方案。架構

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

ScopedModel是早期Flutter社區封裝的比較成功的狀態管理組件。它把狀態封裝入Model裏,修改數據的邏輯也在model裏,經過跟InheritedWidget相似的方式把Model傳給子組件,子組件能夠從Model取數據,也能夠直接調用Model的方法修改數據。

不事後來Flutter官方選擇了更優秀且一樣方便的狀態管理框架provider進行推廣,這裏就很少介紹ScopedModel了。

provider

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還提供了另外幾種方式進行數據管理:

  1. Provider
    • 單純共享數據給子組件,可是數據更新時不會通知子組件。
  2. ListenableProvider
    • 比起ChangeNotifierProvider區別主要是不會自動調用ChangeNotifier.dispose釋放資源。通常不用。
  3. ValueListenableProvider
    • 能夠認爲是ChangeNotifierProvider的特例,只監聽一個數據的時候,使用ValueListenableProvider在修改數據時能夠不用調用notifyListeners()
  4. StreamProvider
    • 用於監聽一個Stream
  5. FutureProvider
    • 提供了一個 Future 給其子孫節點,並在 Future 完成時,通知依賴的子孫節點進行刷新

Redux(flutter_redux)

前面幾個庫,大致上來說,只是提供了基本的數據管理能力,也就是,專心作好本身數據通道的職責,並未對具體實現作過多的限制的話。

相比之下,Redux就重得多了,它不僅是作了個數據通道,還對整個數據層的操做進行了比較強的約束,至關於提供了比較完整的數據層設計模式。場景比較簡單的話,這會顯得代碼很累贅;但項目比較大場景比較複雜的話,這些約束能夠有效阻止代碼的腐爛。

咱們來看一下Redux的核心概念:

img

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部分也講得很清楚了。

fish-redux

剛剛,阿里宣佈開源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應當有比較豐富的經驗,而且,不建議簡單應用使用這麼重的框架。

BloC

基於流的響應式狀態管理,由Google在2018的 DartConf首次提出。

毫無疑問,Stream或者更進一步的ReactiveX是純粹的響應式編程,但其實Redux也經過Action、Reducer這樣的方式實現了響應式(或許某種程度上不是特別純粹,對異步的處理可能比起Stream的方式略有不足),所以實際使用時BloC和Redux其實會有必定的類似性。

image.png

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,功能豐富,使用簡單,性能優化能力也提供得比較完整。

相關文章
相關標籤/搜索