天天看點

Flutter如何狀态管理

目錄介紹

  • 01.什麼是狀态管理
  • 02.狀态管理方案分類
  • 03.狀态管理使用場景
  • 04.Widget管理自己的狀态
  • 05.Widget管理子Widget狀态
  • 06.簡單混合管理狀态
  • 07.全局狀态如何管理
  • 08.Provider使用方法
  • 09.訂閱監聽修改狀态

推薦

  • 響應式的程式設計架構中都會有一個永恒的主題——“狀态(State)管理”
    • 在Flutter中,想一個問題,

      StatefulWidget

      的狀态應該被誰管理?
    • Widget本身?父Widget?都會?還是另一個對象?答案是取決于實際情況!
  • 以下是管理狀态的最常見的方法:
    • Widget管理自己的狀态。
    • Widget管理子Widget狀态。
    • 混合管理(父Widget和子Widget都管理狀态)。
    • 不同子產品的狀态管理。
  • 如何決定使用哪種管理方法?下面給出的一些原則可以幫助你做決定:
    • 如果狀态是使用者資料,如複選框的選中狀态、滑塊的位置,則該狀态最好由父Widget管理。
    • 如果狀态是有關界面外觀效果的,例如顔色、動畫,那麼狀态最好由Widget本身來管理。
    • 如果某一個狀态是不同Widget共享的則最好由它們共同的父Widget管理。
    • 如果是多個子產品需要公用一個狀态,那麼該怎麼處理呢,那可以用Provider。
    • 如果修改了某一個屬性,需要重新整理多個地方資料。比如修改使用者城市id資料,那麼則重新整理首頁n處的接口資料,這個時候可以用訂閱監聽修改狀态

  • setState狀态管理
    • 優點:
      • 簡單場景下特别适用,邏輯簡單,易懂易實作
      • 所見即所得,效率比較高
    • 缺點
      • 邏輯與視圖耦合嚴重,複雜邏輯下可維護性很差
      • 資料傳輸基于依賴傳遞,層級較深情況下不易維護,可讀性差
  • InheritedWidget狀态管理
    • 優點
      • 友善資料傳輸,可以基于InheritedWidget達到邏輯和視圖解耦的效果
      • flutter内嵌類,基礎且穩定,無代碼侵入
      • 屬于比較基礎的類,友好性不如封裝的第三方庫
      • 對于性能需要額外注意,重新整理範圍如果過大會影響性能
  • Provider狀态管理
      • 功能完善,涵蓋了ScopedModel和InheritedWidget的所有功能
      • 資料邏輯完美融入了widget樹中,代碼結構清晰,可以管理局部狀态和全局狀态
      • 解決了多model和資源回收的問題
      • 對不同場景下使用的provider做了優化和區分
      • 支援異步狀态管理和provider依賴注入
      • 使用不當可能會造成性能問題(大context引起的rebuild)
      • 局部狀态之前的資料同步不支援
  • 訂閱監聽修改狀态
    • 有兩種:一種是bus事件通知(是一種訂閱+觀察),另一個是接口注冊回調。
    • 接口回調:由于使用了回調函數原理,是以資料傳遞實時性非常高,相當于直接調用,一般用在功能子產品上。
    • bus事件:元件之間的互動,很大程度上降低了它們之間的耦合,使得代碼更加簡潔,耦合性更低,提升我們的代碼品質。

    • 适合Widget管理自己的狀态,這種很常見,調用setState重新整理自己widget改變狀态。
    • 适合Widget管理子Widget狀态,這種也比較常見。不過這種關聯性比較強。

  • _TapboxAState 類:
    • 管理TapboxA的狀态。
    • 定義

      _active

      :确定盒子的目前顔色的布爾值。
    • _handleTap()

      函數,該函數在點選該盒子時更新

      _active

      ,并調用

      setState()

      更新UI。
    • 實作widget的所有互動式行為。
  • 代碼如下
    // TapboxA 管理自身狀态.
    
    //------------------------- TapboxA ----------------------------------
    
    class TapboxA extends StatefulWidget {
      TapboxA({Key key}) : super(key: key);
    
      @override
      _TapboxAState createState() => new _TapboxAState();
    }
    
    class _TapboxAState extends State<TapboxA> {
      bool _active = false;
    
      void _handleTap() {
        setState(() {
          _active = !_active;
        });
      }
    
      Widget build(BuildContext context) {
        return new GestureDetector(
          onTap: _handleTap,
          child: new Container(
            child: new Center(
              child: new Text(
                _active ? 'Active' : 'Inactive',
                style: new TextStyle(fontSize: 32.0, color: Colors.white),
              ),
            ),
            width: 200.0,
            height: 200.0,
            decoration: new BoxDecoration(
              color: _active ? Colors.lightGreen[700] : Colors.grey[600],
            ),
          ),
        );
      }
    }           

  • 先看一下下面這些是什麼
    typedef ValueChanged<T> = void Function(T value);           
  • 對于父Widget來說,管理狀态并告訴其子Widget何時更新通常是比較好的方式。
    • 例如,

      IconButton

      是一個圖示按鈕,但它是一個無狀态的Widget,因為我們認為父Widget需要知道該按鈕是否被點選來采取相應的處理。
    • 在以下示例中,TapboxB通過回調将其狀态導出到其父元件,狀态由父元件管理,是以它的父元件為

      StatefulWidget

  • ParentWidgetState

    類:
    • 為TapboxB 管理

      _active

      狀态。
    • 實作

      _handleTapboxChanged()

      ,當盒子被點選時調用的方法。
    • 當狀态改變時,調用

      setState()

  • TapboxB 類:
    • 繼承

      StatelessWidget

      類,因為所有狀态都由其父元件處理。
    • 當檢測到點選時,它會通知父元件。
      // ParentWidget 為 TapboxB 管理狀态.
      
      class ParentWidget extends StatefulWidget {
        @override
        _ParentWidgetState createState() => new _ParentWidgetState();
      }
      
      class _ParentWidgetState extends State<ParentWidget> {
        bool _active = false;
      
        void _handleTapboxChanged(bool newValue) {
          setState(() {
            _active = newValue;
          });
        }
      
        @override
        Widget build(BuildContext context) {
          return new Scaffold(
            appBar: new AppBar(
              title: new Text("Widget管理子Widget狀态"),
            ),
            body: new ListView(
              children: [
                new Text("Widget管理子Widget狀态"),
                new TapboxB(
                  active: _active,
                  onChanged: _handleTapboxChanged,
                ),
              ],
            ),
          );
        }
      }
      
      //------------------------- TapboxB ----------------------------------
      
      class TapboxB extends StatefulWidget{
      
        final bool active;
        final ValueChanged<bool> onChanged;
      
        TapboxB({Key key , this.active : false ,@required this.onChanged });
      
        @override
        State<StatefulWidget> createState() {
          return new TabboxBState();
        }
      
      }
      
      class TabboxBState extends State<TapboxB>{
      
        void _handleTap() {
          widget.onChanged(!widget.active);
        }
      
        @override
        Widget build(BuildContext context) {
          return new GestureDetector(
            onTap: _handleTap,
            child: new Container(
              child: new Center(
                child: new Text(
                  widget.active ? 'Active' : 'Inactive',
                ),
              ),
              width: 100,
              height: 100,
              decoration: new BoxDecoration(
                color: widget.active ? Colors.lightGreen[700] : Colors.grey[850],
              ),
            ),
          );
        }
      }           

  • 對于一些元件來說,混合管理的方式會非常有用。
    • 在這種情況下,元件自身管理一些内部狀态,而父元件管理一些其他外部狀态。
  • 在下面TapboxC示例中
    • 手指按下時,盒子的周圍會出現一個深綠色的邊框,擡起時,邊框消失。點選完成後,盒子的顔色改變。
    • TapboxC将其

      _active

      狀态導出到其父元件中,但在内部管理其

      _highlight

    • 這個例子有兩個狀态對象

      _ParentWidgetState

      _TapboxCState

  • _ParentWidgetStateC

    • 管理

      _active

    • _handleTapboxChanged()

      ,當盒子被點選時調用。
    • 當點選盒子并且

      _active

      狀态改變時調用

      setState()

  • _TapboxCState

    對象:
    • _highlight

    • GestureDetector

      監聽所有tap事件。當使用者點下時,它添加高亮(深綠色邊框);當使用者釋放時,會移除高亮。
    • 當按下、擡起、或者取消點選時更新

      _highlight

      狀态,調用

      setState()

    • 當點選時,将狀态的改變傳遞給父元件。
    //---------------------------- ParentWidget ----------------------------
    
    class ParentWidgetC extends StatefulWidget {
      @override
      _ParentWidgetCState createState() => new _ParentWidgetCState();
    }
    
    class _ParentWidgetCState extends State<ParentWidgetC> {
      bool _active = false;
    
      void _handleTapboxChanged(bool newValue) {
        setState(() {
          _active = newValue;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          appBar: new AppBar(
            title: new Text("簡單混合管理狀态"),
          ),
          body: new Container(
            child: new ListView(
              children: [
                new Text("_ParentWidgetCState狀态管理"),
                new Padding(padding: EdgeInsets.all(10)),
                new Text(
                  _active ? 'Active' : 'Inactive',
                ),
                new Padding(padding: EdgeInsets.all(10)),
                new Text("_TapboxCState狀态管理"),
                new TapboxC(
                  active: _active,
                  onChanged: _handleTapboxChanged,
                )
              ],
            ),
          ),
        );
      }
    }
    
    //----------------------------- TapboxC ------------------------------
    
    class TapboxC extends StatefulWidget {
      TapboxC({Key key, this.active: false, @required this.onChanged})
          : super(key: key);
    
      final bool active;
      final ValueChanged<bool> onChanged;
      
      @override
      _TapboxCState createState() => new _TapboxCState();
    }
    
    class _TapboxCState extends State<TapboxC> {
      bool _highlight = false;
    
      void _handleTapDown(TapDownDetails details) {
        setState(() {
          _highlight = true;
        });
      }
    
      void _handleTapUp(TapUpDetails details) {
        setState(() {
          _highlight = false;
        });
      }
    
      void _handleTapCancel() {
        setState(() {
          _highlight = false;
        });
      }
    
      void _handleTap() {
        widget.onChanged(!widget.active);
      }
    
      @override
      Widget build(BuildContext context) {
        // 在按下時添加綠色邊框,當擡起時,取消高亮  
        return new GestureDetector(
          onTapDown: _handleTapDown, // 處理按下事件
          onTapUp: _handleTapUp, // 處理擡起事件
          onTap: _handleTap,
          onTapCancel: _handleTapCancel,
          child: new Container(
            child: new Center(
              child: new Text(widget.active ? 'Active' : 'Inactive',
                  style: new TextStyle(fontSize: 32.0, color: Colors.white)),
            ),
            width: 200.0,
            height: 200.0,
            decoration: new BoxDecoration(
              color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
              border: _highlight
                  ? new Border.all(
                      color: Colors.teal[700],
                      width: 10.0,
                    )
                  : null,
            ),
          ),
        );
      }
    }           

  • 當應用中需要一些跨元件(包括跨路由)的狀态需要同步時,上面介紹的方法便很難勝任了。
    • 比如,我們有一個設定頁,裡面可以設定應用的語言,我們為了讓設定實時生效,我們期望在語言狀态發生改變時,APP中依賴應用語言的元件能夠重新build一下,但這些依賴應用語言的元件和設定頁并不在一起,是以這種情況用上面的方法很難管理。
    • 這時,正确的做法是通過一個全局狀态管理器來處理這種相距較遠的元件之間的通信。
  • 目前主要有兩種辦法:
    • 1.實作一個全局的事件總線,将語言狀态改變對應為一個事件,然後在APP中依賴應用語言的元件的

      initState

      方法中訂閱語言改變的事件。當使用者在設定頁切換語言後,我們釋出語言改變事件,而訂閱了此事件的元件就會收到通知,收到通知後調用

      setState(...)

      方法重新

      build

      一下自身即可。
    • 2.使用一些專門用于狀态管理的包,如Provider、Redux,讀者可以在pub上檢視其詳細資訊。
  • 舉一個簡答的案例來實踐
    • 本執行個體中,使用Provider包來實作跨元件狀态共享,是以我們需要定義相關的Provider。
    • 需要共享的狀态有登入使用者資訊、APP主題資訊、APP語言資訊。由于這些資訊改變後都要立即通知其它依賴的該資訊的Widget更新,是以我們應該使用

      ChangeNotifierProvider

      ,另外,這些資訊改變後都是需要更新Profile資訊并進行持久化的。
    • 綜上所述,我們可以定義一個

      ProfileChangeNotifier

      基類,然後讓需要共享的Model繼承自該類即可,

      ProfileChangeNotifier

      定義如下:
      class ProfileChangeNotifier extends ChangeNotifier {
        Profile get _profile => Global.profile;
      
        @override
        void notifyListeners() {
          Global.saveProfile(); //儲存Profile變更
          super.notifyListeners(); //通知依賴的Widget更新
        }
      }           
    • 使用者狀态
      • 使用者狀态在登入狀态發生變化時更新、通知其依賴項,我們定義如下:
      class UserModel extends ProfileChangeNotifier {
        User get user => _profile.user;
      
        // APP是否登入(如果有使用者資訊,則證明登入過)
        bool get isLogin => user != null;
      
        //使用者資訊發生變化,更新使用者資訊并通知依賴它的子孫Widgets更新
        set user(User user) {
          if (user?.login != _profile.user?.login) {
            _profile.lastLogin = _profile.user?.login;
            _profile.user = user;
            notifyListeners();
          }
        }
      }           

8.1 正确地初始化 Provider

  • 如下所示,create是必須要傳遞的參數
    ChangeNotifierProvider(
      create: (_) => MyModel(),
      child: ...
    )           
  • 實際開發中如何應用
    builder: (BuildContext context, Widget child) {
        return MultiProvider(providers: [
          ChangeNotifierProvider(create: (context) => BusinessPattern()),
        ]);
    },           
  • 然後看一下BusinessPattern是什麼?
    class BusinessPattern extends ChangeNotifier {
      PatternState currentState = PatternState.none;
      void updateBusinessPatternState(PatternState state) {
        if (currentState.index != state.index) {
          LogUtils.d('目前模式:$currentState');
          LogUtils.d('更新模式:$state');
          currentState = state;
          notifyListeners();
        }
      }
    }           

8.2 如何擷取Provider取值

  • 一種是 Provider.of(context) 比如:
    Widget build(BuildContext context) {
      final text = Provider.of<String>(context);
      return Container(child: Text(text));
    }           
    • 遇到的問題:由于 Provider 會監聽 Value 的變化而更新整個 context 上下文,是以如果 build 方法傳回的 Widget 過大過于複雜的話,重新整理的成本是非常高的。那麼我們該如何進一步控制 Widget 的更新範圍呢?
    • 解決辦法:一個辦法是将真正需要更新的 Widget 封裝成一個獨立的 Widget,将取值方法放到該 Widget 内部。
    Widget build(BuildContext context) {
      return Container(child: MyText());
    }
    
    class MyText extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final text = Provider.of<String>(context);
        return Text(text);
      }
    }           
  • Consumer 是 Provider 的另一種取值方式
    • Consumer 可以直接拿到 context 連帶 Value 一并傳作為參數傳遞給 builder ,使用無疑更加友善和直覺,大大降低了開發人員對于控制重新整理範圍的工作成本。
    Widget getWidget2(BuildContext context) {
        return Consumer<BusinessPattern>(builder: (context, businessModel, child) {
          switch (businessModel.currentState) {
            case PatternState.none:
              return  Text("無模式");
              break;
            case PatternState.normal:
              return Text("正常模式");
              break;
            case PatternState.small:
              return Text("小屏模式");
              break;
            case PatternState.overview:
              return Text("全屏模式");
              break;
            default:
              return Text("其他模式");
              return SizedBox();
          }
      });
    }           
  • Selector 是 Provider 的另一種取值方式
    • Selector 是 3.1 推出的功能,目的是更近一步的控制 Widget 的更新範圍,将監聽重新整理的範圍控制到最小
    • selector:是一個 Function,傳入 Value ,要求我們傳回 Value 中具體使用到的屬性。
    • shouldRebuild:這個 Function 會傳入兩個值,其中一個為之前保持的舊值,以及此次由 selector 傳回的新值,我們就是通過這個參數控制是否需要重新整理 builder 内的 Widget。如果不實作 shouldRebuild ,預設會對 pre 和 next 進行深比較(deeply compares)。如果不相同,則傳回 true。
    • builder:傳回 Widget 的地方,第二個參數 定義的參數,就是我們剛才 selector 中傳回的 參數。
    Widget getWidget4(BuildContext context) {
      return Selector<BusinessPattern, PatternState>(
        selector: (context, businessPattern) =>
        businessPattern.currentState,
        builder: (context, state, child) {
          switch (state) {
            case PatternState.none:
              return  Text("無模式");
              break;
            case PatternState.normal:
              return Text("正常模式");
              break;
            case PatternState.small:
              return Text("小屏模式");
              break;
            case PatternState.overview:
              return Text("全屏模式");
              break;
            default:
              return Text("其他模式");
              return SizedBox();
          }
      }
    );           

8.3 修改Provider狀态

  • 如何調用修改狀态管理
    BusinessPatternService _patternService = serviceLocator<BusinessPatternService>();
    //修改狀态
    _patternService.nonePattern();
    _patternService.normalPattern();           
  • 然後看一下normalPattern的具體實作代碼
    class BusinessPatternServiceImpl extends BusinessPatternService {
    
      final BuildContext context;
      BusinessPatternServiceImpl(this.context);
    
      PatternState get currentPatternState =>
          _getBusinessPatternState(context).currentState;
    
      BusinessPattern _getBusinessPatternState(BuildContext context) {
        return Provider.of<BusinessPattern>(context, listen: false);
      }
    
      @override
      void nonePattern() {
        BusinessPattern _patternState = _getBusinessPatternState(context);
        _patternState.updateBusinessPatternState(PatternState.none);
      }
    
      @override
      void normalPattern() {
        BusinessPattern _patternState = _getBusinessPatternState(context);
        _patternState.updateBusinessPatternState(PatternState.normal);
      }
    }           

8.4 關于Provider重新整理

  • 狀态發生變化後,widget隻會重新build,而不會重新建立(重用機制跟key有關,如果key發生變化widget就會重新生成)

  • 首先定義抽象類。還需要寫上具體的實作類
    typedef LocationDataChangedFunction = void Function(double);
    
    abstract class LocationListener {
      /// 注冊資料變化的回調
      void registerDataChangedFunction(LocationDataChangedFunction function);
      /// 移除資料變化的回調
      void unregisterDataChangedFunction(LocationDataChangedFunction function);
      /// 更新資料的變化
      void locationDataChangedCallback(double angle);
    }
    
    
    class LocationServiceCenterImpl extends LocationListener {
    
      List<LocationDataChangedFunction> _locationDataChangedFunction = List();
    
      @override
      void locationDataChangedCallback(double angle) {
        _locationDataChangedFunction.forEach((function) {
          function.call(angle);
        });
      }
    
      @override
      void registerDataChangedFunction(LocationDataChangedFunction function) {
        _locationDataChangedFunction.add(function);
      }
    
      @override
      void unregisterDataChangedFunction(LocationDataChangedFunction function) {
        _locationDataChangedFunction.remove(function);
      }
    }           
  • 那麼如何使用呢?在需要用的頁面添加接口回調監聽
    _locationListener.registerDataChangedFunction(_onDataChange);
    void _onDataChange(double p1) {
      //監聽回調處理
    }           
  • 那麼如何發送事件,這個時候
    LocationListener _locationListener = locationService();
    _locationListener.locationDataChangedCallback(520.0);           

繼續閱讀