Flutter State Management狀態管理全面分析

i校長發表於2020-05-21

前言

2019 Google I/O 大會,google就推出Provider,成為官方推薦的狀態管理方式之一,Flutter 狀態管理一直是個很熱門的話題,而且狀態管理的庫也是超級多,這確實是我們每一個做Flutter開發難以避免的一道坎,既然這麼重要,我們如何去理解它,如何使用它,如何做到更好呢?接下來讓我告訴你答案

囉嗦幾句

該文章已經經歷了一週的迭代,預計還要一週左右,要做一個全面的分析,當然要每個細節都要關注到,如果您覺得好,請不要吝嗇您的大拇指,順便點個贊哦,麼麼噠,如果有不對的地方提出來,一個地方一個紅包獎勵哦,愛你們。

主要內容

一張圖告訴你,我要講的主要內容。下面將圍繞這八個方面來講。七個理論,一個實踐。

  • 狀態管理是什麼
  • 為什麼需要狀態管理
  • 狀態管理基本分類
  • 狀態管理的底層邏輯
  • 狀態管理的使用原則
  • 使用成熟狀態管理庫的弊端
  • 選擇狀態管理庫的原則
  • Provider 深入分析(學以致用)

狀態管理是什麼

我們知道最基本的程式是什麼:

  • 程式=演算法+資料結構

資料是程式的中心。資料結構和演算法兩個概念間的邏輯關係貫穿了整個程式世界,首先二者表現為不可分割的關係。其實Flutter不就是一個程式嗎,那我們面臨的最底層的問題還是演算法和資料結構,所以我們推匯出

  • Flutter=演算法+資料結構

那狀態管理是什麼?我也用公式來表達一下,如下:

  • Flutter狀態管理=演算法+資料結構+UI繫結

瞬間秒懂有沒有?來看一個程式碼例子:

class ThemeBloc {
  final _themeStreamController = StreamController<AppTheme>();

  get changeTheTheme => _themeStreamController.sink.add;

  get darkThemeIsEnabled => _themeStreamController.stream;

  dispose() {
    _themeStreamController.close();
  }
}

final bloc = ThemeBloc();

class AppTheme {
  ThemeData themeData;

  AppTheme(this.themeData);
}
/// 繫結到UI
StreamBuilder<AppTheme>(
        initialData: AppTheme.LIGHT_THEME,
        stream: bloc.darkThemeIsEnabled,
        builder: (context, AsyncSnapshot<AppTheme> snapshot) {
          return MaterialApp(
            title: 'Jetpack',
            theme: snapshot.data.themeData,
            home: PageHome(),
            routes: <String, WidgetBuilder>{
              "/pageChatGroup": (context) => PageChatGroup(),
              "/LaoMeng": (context) => LaoMeng(),
            },
          );
        })
  • AppTheme 是資料結構
  • changeTheTheme 是演算法
  • StreamBuilder 是繫結UI

這樣一整套程式碼的邏輯就是我們所說的Flutter狀態管理,這樣解釋大家理解了嗎?再細說,演算法就是我們如何管理,資料結構就是資料狀態,狀態管理的本質還是如何通過合理的演算法管理資料,如何取,如何接收等,最終展示在UI上,通過UI的變更來體現狀態的管理邏輯。

為什麼需要

這裡就需要明白一個事情,Flutter的很多優秀的設計都來源於React,對於react來說,同級元件之間的通訊尤為麻煩,或者是非常麻煩了,所以我們把所有需要多個元件使用的state拿出來,整合到頂部容器,進行分發。狀態管理可以實現元件通訊、跨元件資料儲存。推薦閱讀對 React 狀態管理的理解及方案對比,那麼對於Flutter來說呢?你知道Android、Ios等原生於Flutter最本質的區別嗎?來看一段程式碼:

//android
TextView tv = TextView()
tv.setText("text")
///flutter
setState{
    text = "text"
}

從上面程式碼我們看出,Android的狀態變更是通過具體的元件直接賦值,如果頁面全部變更,你是不是需要每一個都設定一遍呢?,而Flutter的變更就簡單粗暴,setState搞定,它背後的邏輯是重新build整個頁面,發現有變更,再將新的資料賦值,其實Android、Ios與flutter的本質的區別就是資料與檢視完全分離,當然Android也出現了UI繫結框架,似乎跟React、Flutter越來越像,所以這也在另一方面凸顯出了,Flutter設計的先進性,沒有什麼創新,但更符合未來感,回過頭來,仔細想一想,這樣設計有什麼弊端?

對了你猜對了:頁面如何重新整理才是Flutter的關鍵,做Android的同學肯定也面臨著一個問題,頁面的重繪導致的丟幀問題,為了更好,我們很多時候都選擇了區域性重新整理來優化對吧,Android、Ios已經很明確的告訴UI要重新整理什麼更新什麼,而對於Flutter來說,這一點很不清晰,雖然Flutter也做了類似虛擬Dom優化重繪邏輯,但這些遠遠不夠的,如何合理的更新UI才是最主要的,這個時候一大堆的狀態管理就出來了,當然狀態管理也不是僅僅為了解決更新問題。

我再丟擲一個問題,如果我有一個widget A,我想在另外一個widget B中改變widget A的一個狀態,或者從網路、資料庫取到資料,然後重新整理它,怎麼做?我們來模擬一下,來看程式碼

糟糕的狀態管理程式碼

class WidgetTest extends StatefulWidget {
  @override
  _WidgetTestState createState() => _WidgetTestState();
}

class _WidgetTestState extends State<WidgetTest> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          WidgetA(),
          WidgetB()
        ],
      ),
    );
  }
}

_WidgetAState _widgetAState;
class WidgetA extends StatefulWidget {
  @override
  _WidgetAState createState() {
    _widgetAState = _WidgetAState();
    return _widgetAState;
  }
}
class _WidgetAState extends State<WidgetA> {
  var title = "";
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(title),
    );
  }
}

class WidgetB extends StatefulWidget {
  @override
  _WidgetBState createState() => _WidgetBState();
}

class _WidgetBState extends State<WidgetB> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        onPressed: () {
          _widgetAState.setState(() {
            _widgetAState.title = "WidgetB";
          });
        },
      ),
    );
  }
}

WidgetTest頁面有兩個widget,分別是WidgetA、WidgetB,WidgetB通過RaisedButton的onPressed來改變WidgetA的Text,怎麼做到的呢,直接用WidgetA的_WidgetAState物件提供的setState函式來變更,沒什麼問題對吧,而且功能實現了,但你仔細思考一下,這有什麼問題呢?

  • _WidgetAState 被全域性化,而且它所有狀態被暴漏出去,如果_WidgetAState有十個狀態,只有一個想讓別人變更,可惜已經晚了, 你加'_'也不行,元件的隱私全沒了
  • 耦合變高,WidgetB有_WidgetAState的強關聯,我們編碼追求的解偶,在這裡完全被忽視了
  • 效能變差,為什麼這麼說?因為每次_widgetAState.setState都會導致整個頁面甚至子Widget的重新build,如果_widgetAState裡面有成千上百的狀態,效能肯定差到極點
  • 不可測,程式變得難以測試

如何變好呢
這就需要選擇一種合適的狀態管理方式。

狀態管理的目標
其實我們做狀態管理,不僅僅是因為它的特點,而為了更好架構,不是嗎?

  • 程式碼要層次分明,易維護,易閱讀
  • 可擴充套件,易維護,可以動態替換UI而不影響演算法邏輯
  • 安全可靠,保持資料的穩定伸縮
  • 效能佳,區域性優化

這些不緊緊是狀態管理的目的,也是我們做一款優秀應用的基礎架構哦。

基本分類

  • 區域性管理 官方也稱 Ephemeral state,意思是短暫的狀態,這種狀態根本不需要做全域性處理

舉個例子,如下方的_index,這就是一個區域性或者短暫狀態,只需要StatefulWidget處理即可完成

class MyHomepage extends StatefulWidget {
  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}
  • 全域性管理 官方稱 App state,即應用狀態,非短暫狀態,您要在應用程式的許多部分之間共享,以及希望在使用者會話之間保持的狀態,就是我們所說的應用程式狀態(有時也稱為共享狀態)

例如:

  • 使用者偏好
  • 登入資訊
  • 購物車
  • 新聞閱讀狀態

狀態分類官方定義

沒有明確的通用規則來區分特定變數是短暫狀態還是應用程式狀態。有時,您必須將一個重構為另一個。例如,您將從一個明顯的短暫狀態開始,但是隨著您的應用程式功能的增長,可能需要將其移至應用程式狀態。 出於這個原因,請使用下圖進行分類:

總之,任何Flutter應用程式中都有兩種概念性的狀態型別。臨時狀態可以使用State和setState()來實現,並且通常是單個視窗小部件的本地狀態。剩下的就是您的應用狀態。兩種型別在任何Flutter應用程式中都有自己的位置,兩者之間的劃分取決於您自己的喜好和應用程式的複雜性


沒有最好的管理方式,只有最合適的管理方式

底層邏輯

底層邏輯我想告訴你的是,Flutter中目前有哪些可以做到狀態管理,有什麼缺點,適合做什麼不適合做什麼,只有你完全明白底層邏輯,才不會畏懼複雜的邏輯,即使是複雜的邏輯,你也能選擇合理的方式去管理狀態。

  • State

StatefulWidget、StreamBuilder狀態管理方式

  • InheritedWidget

專門負責Widget樹中資料共享的功能型Widget,如Provider、scoped_model就是基於它開發

  • Notification

與InheritedWidget正好相反,InheritedWidget是從上往下傳遞資料,Notification是從下往上,但兩者都在自己的Widget樹中傳遞,無法跨越樹傳遞。

  • Stream

資料流 如Bloc、flutter_redux、fish_redux等也都基於它來做實現
為什麼列這些東西?因為現在大部分流行的狀態管理都離不開它們。理解它們比理解那些吹自己牛逼的框架要好的多。請關注底層邏輯,這樣你才能遊刃有餘。下面我們一個個分析一下:

State

State 是我們常用而且使用最頻繁的一個狀態管理類,它必須結合StatefulWidget一起使用,StreamBuilder繼承自StatefulWidget,同樣是通過setState來管理狀態

舉個例子來看下:

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

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

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

引用官方的例子,這裡_active狀態就是通過State提供的setState函式來實現的
為什麼會讓State去管理狀態,而不是Widget本身呢?Flutter設計時讓Widget本身是不變的,類似固定的配置資訊,那麼就需要一個角色來控制它,State就出現了,但State的任何更改都會強制整個Widget重新構建,當然你也可以覆蓋必要方法自己控制邏輯。

再看個例子:

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

從這裡你看出什麼?對了,父元件可以通過setState來重新整理子Widget的狀態變化,所以得出如下觀點
注意
setState是整個Widget重新構建(而且子Widget也會跟著銷燬重建),這個點也是為什麼不推薦你大量使用StatefulWidget的原因。如果頁面足夠複雜,就會導致嚴重的效能損耗。如何優化呢?建議使用StreamBuilder,它原理上也是State,但它做到了子Widget的區域性重新整理,不會導致整個頁面的重建,是不是就好很多了呢?

State缺點

從上面的程式碼我們分析一下它的缺點

  • 無法做到跨元件共享資料(這個跨是無關聯的,如果是直接的父子關係,我們不認為是跨元件)

setState是State的函式,一般我們會將State的子類設定為私有,所以無法做到讓別的元件呼叫State的setState函式來重新整理

  • setState會成為維護的難點,因為啥哪哪都是。

隨著頁面狀態的增多,你可能在呼叫setState的地方會越來越多,不能統一管理

  • 處理資料邏輯和檢視混合在一起,違反程式碼設計原則

比如資料庫的資料取出來setState到Ui上,這樣編寫程式碼,導致狀態和UI耦合在一起,不利於測試,不利於複用。

State小結

當然反過來講,不是因為它有缺點我們就不使用了,我們追求的簡單高效,簡單實現,高效執行,當複雜到需要更好的管理的時候再重構。一個基本原則就是,狀態是否需要跨元件使用,如果需要那就用別的辦法管理狀態而不是State管理。

InheritedWidget

InheritedWidget是一個無私的Widget,它可以把自己的狀態資料,無私的交給所有的子Widget,所有的子Widget可以無條件的繼承它的狀態。就這麼一個東西。有了State我們為什麼還需要它呢?我們已經知道,State是可以更新直接子Widget的狀態,但如果是子Widget的子Widget呢,所以說InheritedWidget的存在,一是為了更簡單的獲取狀態,二是大家都共享這個狀態,舉個例子

class InheritedWidgetDemo extends InheritedWidget {

  final int accountId;

  InheritedWidgetDemo(this.accountId, {Key key, Widget child})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidgetDemo old) =>
      accountId != old.accountId;

  static InheritedWidgetDemo of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedWidgetDemo>();
  }

}

class MyPage extends StatelessWidget {
  final int accountId;

  MyPage(this.accountId);

  Widget build(BuildContext context) {
    return new InheritedWidgetDemo(
      accountId,
      child: const MyWidget(),
    );
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget();

  Widget build(BuildContext context) {
    return MyOtherWidget();
  }
}

class MyOtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myInheritedWidget = InheritedWidgetDemo.of(context);
    print(myInheritedWidget.accountId);
  }

}
  • InheritedWidgetDemo共享狀態accountId給了MyOtherWidget,而MyOtherWidget是MyWidget的子Widget,這就是InheritedWidget的功效,它可以做到跨元件共享狀態。
  • const MyWidget() 表示該Widget是常量,不會因為頁面的重新整理導致重新build,這就是優化的細節,這裡想一下,如果你用State實現,不是就需要它setState才能實現MyOtherWidget的重新build,這樣做的壞處就是導致整個UI的重新整理。
  • updateShouldNotify 它也是一個優化點,在你橫屏變豎屏的同時,導致整個UI重新build,可由於updateShouldNotify的判斷,系統將不會重新build MyOtherWidget,也是一種佈局優化。
  • 子樹中的元件通過InheritedWidgetDemo.of(context)訪問共享狀態。

有的人想了,InheritedWidget這麼好用,那我把整個App的狀態都存進來怎麼樣?類似這樣

class AppContext {
  int teamId;
  String teamName;
  
  int studentId;
  String studentName;
  
  int classId;
  ...
}

其實這樣不好,我們不光是要做技術上的元件化,更要關注的是業務,對業務的充分理解並實現模組化分工,在使用InheritedWidget時候特別是要注意這一點,更推薦你使用該方案:

class TeamContext {
  int teamId;
  String teamName;
}

class StudentContext {
  int studentId;
  String studentName;
}
 
class ClassContext {
  int classId;
  ...
}

注意
它的資料是隻讀的,雖然很無私,但子widget不能修改,那麼如何修改呢?
舉個例子:

class Item {
   String reference;

   Item(this.reference);
}

class _MyInherited extends InheritedWidget {
  _MyInherited({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final MyInheritedWidgetState data;

  @override
  bool updateShouldNotify(_MyInherited oldWidget) {
    return true;
  }
}

class MyInheritedWidget extends StatefulWidget {
  MyInheritedWidget({
    Key key,
    this.child,
  }): super(key: key);

  final Widget child;

  @override
  MyInheritedWidgetState createState() => new MyInheritedWidgetState();

  static MyInheritedWidgetState of(BuildContext context){
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedWidgetState extends State<MyInheritedWidget>{
  /// List of Items
  List<Item> _items = <Item>[];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  /// Helper method to add an Item
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State<MyTree> {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: <Widget>[
            new WidgetA(),
            new Container(
              child: new Row(
                children: <Widget>[
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

該例子引用自widget-state-context-inheritedwidget/歡迎閱讀學習哦

  • _MyInherited是InheritedWidget,每次我們通過單擊“ Widget A”的按鈕新增元素時都會重新建立
  • MyInheritedWidget是一個狀態為包含元素列表的視窗小部件。可通過(BuildContext上下文)的靜態MyInheritedWidgetState訪問此狀態
  • MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便子控制元件樹的一部分的控制元件可以使用它們
  • 每次我們向State新增元素時,都會重新構建MyInheritedWidgetState
  • MyTree類僅構建一個小部件樹,將MyInheritedWidget作為該樹的父級
  • WidgetA是一個簡單的RaisedButton,按下該按鈕時,會呼叫最近的MyInheritedWidget的addItem方法。
  • WidgetB是一個簡單的Text,它顯示在最接近的 MyInheritedWidget級別上顯示的元素數量

看了一下日誌輸出如圖:

有沒有發現一個問題?當MyInheritedWidgetState.addItem,導致setState被呼叫,然後就觸發了WidgetA、WidgetB的build的方法,而WidgetA根本不需要重新build,這不是浪費嗎?那麼我們如何優化呢?

static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
                    : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

通過抽象rebuild屬性來控制是否需要重新build

    final MyInheritedWidgetState state = MyInheritedWidget.of(context,false);

然後用的時候加以引數控制,改完程式碼,再看下日誌:

看,已經生效了。你現在是不是對InheritedWidget有了更清晰的認識了呢?但說到它就不得不提InheritedModel,它是InheritedWidget的子類,InheritedModel可以做到部分資料改變的時候才會重建,你可以修改上面例子

class _MyInheritedWidget extends InheritedModel {

  static MyInheritedWidgetState of(BuildContext context, String aspect) {
     return InheritedModel.inheritFrom<_MyInheritedWidget>(context, aspect: aspect).data;
   }

   @override
   bool updateShouldNotifyDependent(_MyInheritedWidget old, Set aspects) {
     return aspects.contains('true');
   }
 }

呼叫修改為:

///不允許重新build
 final MyInheritedWidgetState state = MyInheritedWidget.of(context,"false");
///允許重新build
final MyInheritedWidgetState state = MyInheritedWidget.of(context,"true");

推薦閱讀

inheritedmodel-vs-inheritedwidget

https://juju.one/inheritedwidget-inheritedmodel/

widget-state-context-inheritedwidget/

InheritedWidget 缺點

通過上面的分析,我們來看下它的缺點

  • 容易造成不必要的重新整理
  • 不支援跨頁面(route)的狀態,意思是跨樹,如果不在一個樹中,我們無法獲取
  • 資料是不可變的,必須結合StatefulWidget、ChangeNotifier或者Steam使用
InheritedWidget 小結

經過一系列的舉例和驗證,你也基本的掌握了InheritedWidget了吧,這個元件特別適合在同一樹型Widget中,抽象出公有狀態,每一個子Widget或者孫Widget都可以獲取該狀態,我們還可以通過手段控制rebuild的粒度來優化重繪邏輯,但它更適合從上往下傳遞,如果是從下往上傳遞,我們如何做到呢?請往下看,馬上給你解答

Notification

它是Flutter中跨層資料共享的一種機制,注意,它不是widget,它提供了dispatch方法,來讓我們沿著context對應的Element節點向上逐層傳送通知

具個簡單例子看下

class TestNotification extends Notification {
  final int test;

  TestNotification(this.test);
}

var a = 0;

// ignore: must_be_immutable
class WidgetNotification extends StatelessWidget {

  final String btnText;

  WidgetNotification({Key key, this.btnText}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        child: Text(btnText),
        onPressed: () {
          var b = ++a;
          debugPrint(b.toString());
          TestNotification(b).dispatch(context);
        },
      ),
    );
  }
}

class WidgetListener extends StatefulWidget {
  @override
  _WidgetListenerState createState() => _WidgetListenerState();
}

class _WidgetListenerState extends State<WidgetListener> {
  int _test = 1;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          NotificationListener<TestNotification>(
            child: Column(
              children: <Widget>[
                Text("監聽$_test"),
                WidgetNotification(btnText: "子Widget",)
              ],
            ),
            onNotification: (TestNotification notification) {
              setState(() {
                _test = notification.test;
              });
              return true;
            },
          ),
          WidgetNotification(btnText: "非子Widget",)
        ],
      ),
    );
  }
}
  • 定義TestNotification通知的實現
  • WidgetNotification 負責通知結果,通過RaisedButton的點選事件,將資料a傳遞出去,通過Notification提供的dispatch方法向上傳遞
  • WidgetListener通過Widget NotificationListener來監聽資料變化,最終通過setState變更資料
  • WidgetNotification 例項化了兩次,一次在NotificationListener的樹內部,一個在NotificationListener的外部,經過測試發現,在外部的WidgetNotification並不能通知到內容變化。

所以說在使用Notification的時候要注意,如果遇到無法收到通知的情況,考慮是否是Notification 未在NotificationListener的內部發出通知,這個一定要注意。

同樣的思路,我想看下Notification是如何重新整理Ui的
在程式碼里加入了跟通知無關緊要的WidgetC


這麼看來,你以為是Notification導致的嗎?我把這個註釋掉,如圖

再執行看下,連續點選了八次

原來是State的原因,那麼這種情況我們如何優化呢?這就用到了Stream了,請接著往下繼續看哦。

推薦閱讀

flutter-notifications-bubble-up-and-values-go-down

notification

Notification缺點
  • 不支援跨頁面(route)的狀態,準備的說不支援NotificationListener同級或者父級Widget的狀態通知
  • 本身不支援重新整理UI,需要結合State使用
  • 如果結合State,會導致整個UI的重繪,效率底下不科學
Notification小結

使用起來很簡單,但在重新整理UI方面需要注意,如果頁面複雜度很高,導致無關緊要的元件跟著重新整理,得不償失,還需要另找蹊徑,躲開這些坑,下面我來介紹如何完美躲閃,重磅來襲Stream。

Stream

Stream其實就是一個生產者消費者模型,一端負責生產,一端負責消費,而且是純Dart的實現,跟Flutter沒什麼關係,扯上關係的就是用StreamBuilder來構建一個Stream通道的Widget,像知名的rxdart、BloC、flutter_redux全都用到了Stream的api。所以學習它才是我們掌握狀態管理的一個關鍵

我們先來看下如何改造上面Notification的例子達到我們想要的重新整理效果

///step1
class TestBloc {

  final _testStreamController = StreamController<int>();

  get changeTest => _testStreamController.sink.add;

  get testStream => _testStreamController.stream;

  dispose() {
    _testStreamController.close();
  }
}

final testBloc = TestBloc();

/// step2
StreamBuilder<int>(
                  initialData: 0,
                  stream: testBloc.testStream,
                  builder: (context, snapshot) {
                    return Text("監聽${snapshot.data}");
                  }
                )
/// step3
onNotification: (TestNotification notification) {
              testBloc.changeTest(notification.test);
              return true;
            }
  • 第一步定義TestBloc,負責管理Stream,提供changeTest函式來往Stream通道新增元素
  • 將TestBloc的流testStream賦值給StreamBuilder的stream屬性加以繫結
  • 在收到通知的地方用 testBloc.changeTest來往stream中新增元素,最終由StreamBuilder的setState更新,對的StreamBuilder是通過State重新整理的,想深入的可以看下面推薦閱讀內容,講的很好

看下執行效果

看見了吧,無關緊要的Widget已經不跟著重新整理了,這就是Stream的重要性

推薦閱讀

我自己寫的StreamBuilder原始碼分析

大神寫的Stream全面分析

響應式程式設計:從 Streams 到 BLoC

Stream 缺點

再好的東西,我們更應該關注下它的缺點

  • api生澀,不好理解
  • 需要定製化,才能滿足更復雜的場景
  • 沒有自動dispose邏輯 我們做開發都有個習慣,當這個流不被使用的時候,喜歡close掉,可惜Stream並沒有提供這樣的api,需要自己擴充套件實現,大部分人都是使用StatefulWidget的dispose函式來輔助流的close呼叫,那我們不想使用StatefulWidget怎麼辦,感覺它寫法太麻煩

我們如何做到讓Stream自動Close掉呢?

  • 一種辦法是自己實現擴充套件StreamBuilder,在它dispose的時候呼叫,因為StreamBuilder是Statefulwidget的子類可以覆蓋dispose函式
  • 第二是參考Provider的實現,經過原始碼分析Provider的原理是依賴於InheritedElement的unmount函式實現的,最終回撥函式dispose,unmount函式類似於Android Activity的Destroy函式,頁面徹底銷燬了,不需要任何資料資源了,都釋放了得了,InheritedElement是InheritedWidget的虛擬Dom物件,Flutter 頁面繪製三板斧嘛(widget、element、renderObject)

第二種不太現實,實現起來邏輯複雜,但我想告訴有這麼一個思路,你可以參考。

缺點恰恰是它的優點,保證了足夠靈活,你更可基於它做一個好的設計,滿足當下業務的設計。

小結

通過對State、InheritedWidget、Notification、Stream的學習,你是不是覺得,Flutter的狀態管理也就這些了呢?不一定哈,我也在不斷的學習,如果碰到新的技術,繼續分享給你們哦。難道這就完了嗎?沒有,其實我們只是學了第一步,是什麼,如何用,還沒有討論怎麼用好呢?需要什麼標準嗎,當然有,下面通過我的專案實戰經驗來提出一個基本原則,超過這個原則你就是在破壞平衡,請往下看。

狀態管理的使用原則

區域性管理優於全域性

這個原則來源於,Flutter的效能優化,區域性重新整理肯定比全域性重新整理要好很多,那麼我們在管理狀態的同時,也要考慮該狀態到底是區域性還是全域性,從而編寫正確的邏輯。

保持資料安全性

用“_”私有化狀態,因為當開發人員眾多,當別人看到你的變數的時候,第一反應可能不是找你提供的方法,而是直接對變數操作,那就有可能出現想不到的後果,如果他只能呼叫你提供的方法,那他就要遵循你方法的邏輯,避免資料被處理錯誤。

考慮頁面重新build帶來的影響

很多時候頁面的重建都會呼叫build函式,也就是說,在一個生命週期內,build函式是多次被呼叫的,所以你就要考慮資料的初始化或者重新整理怎麼樣才能合理。

使用成熟狀態管理庫弊端

  • 增加程式碼複雜性
  • 框架bug修復需要時間等待
  • 不理解框架原理導致使用方式不對,反而帶來更多問題
  • 選型錯誤導致不符合應用要求
  • 與團隊風格衝突不適用

通過了解它們的弊端來規避一些風險,綜合考慮,選框架不易,且行且珍惜。

選型原則

  • 侵入性
  • 擴充套件性
  • 高效能
  • 安全性
  • 駕馭性
  • 易用性
  • 範圍性

所有的框架都有侵入性,你同意嗎?不同意請左轉,前面有個坑,你可以跳過去。目前侵入性比較高的代表ScopedModel,為啥?因為它是用extend實現的,需要繼承實現的基本不是什麼好實現,你同意嗎?同上。
擴充套件性就不用說了,如果你選擇的框架只能使用它提供的幾個入口,那麼請你放棄使用它。高效能也是很重要的,這個需要明白它的原理,看它到底如何做的管理。安全性也很重要,看他資料管理通道是否安全穩定。駕馭性,你說你都不理解你就敢用,出了問題找誰?如果駕馭不了也不要用。易用性大家應該都明白,如果用它一個框架需要N多配置,N多實現,放棄吧,不合適。簡單才是硬道理。

範圍性
這個特點是flutter中比較明顯的,框架選型一定要考慮框架的適用範圍,到底是適合做區域性管理,還是適合全域性管理,要做一個實際的考量。

推薦用法

如果是初期,建議多使用Stream、State、Notification來自行處理,順便學習原始碼,多理解,多實踐。有架構能力的就可以著手封裝了,提供更簡單的使用方式
如果是後期,當然也是在前面的基礎之上,再去考慮使用Provider、redux等複雜的框架,原則上要吃透原始碼,否則不建議使用。

注意

你以為使用框架就能萬事大吉了?效能優化是一個不變的話題,包括Provider在內的,如果你使用不當,照樣出現頁面的效能損耗嚴重,所以你又回到了為啥會這樣,請你學習上面的底層邏輯,謝謝?

總結

通過這期分享,你是不是對Flutter的狀態管理有了一個重新的認識呢?如果對你有幫住,請點一下下面的贊哦。謝謝?。

相關文章