Flutter如何狀態管理

楊充發表於2021-08-19

目錄介紹

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

推薦

01.什麼是狀態管理

  • 響應式的程式設計框架中都會有一個永恆的主題——“狀態(State)管理”
    • 在Flutter中,想一個問題,StatefulWidget的狀態應該被誰管理?
    • Widget本身?父Widget?都會?還是另一個物件?答案是取決於實際情況!
  • 以下是管理狀態的最常見的方法:
    • Widget管理自己的狀態。
    • Widget管理子Widget狀態。
    • 混合管理(父Widget和子Widget都管理狀態)。
    • 不同模組的狀態管理。
  • 如何決定使用哪種管理方法?下面給出的一些原則可以幫助你做決定:
    • 如果狀態是使用者資料,如核取方塊的選中狀態、滑塊的位置,則該狀態最好由父Widget管理。
    • 如果狀態是有關介面外觀效果的,例如顏色、動畫,那麼狀態最好由Widget本身來管理。
    • 如果某一個狀態是不同Widget共享的則最好由它們共同的父Widget管理。
    • 如果是多個模組需要公用一個狀態,那麼該怎麼處理呢,那可以用Provider。
    • 如果修改了某一個屬性,需要重新整理多個地方資料。比如修改使用者城市id資料,那麼則重新整理首頁n處的介面資料,這個時候可以用訂閱監聽修改狀態

02.狀態管理方案分類

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

03.狀態管理使用場景

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

04.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],
            ),
          ),
        );
      }
    }
    複製程式碼

05.Widget管理子Widget狀態

  • 先看一下下面這些是什麼

    typedef ValueChanged<T> = void Function(T value);
    複製程式碼
  • 對於父Widget來說,管理狀態並告訴其子Widget何時更新通常是比較好的方式。

    • 例如,IconButton是一個圖示按鈕,但它是一個無狀態的Widget,因為我們認為父Widget需要知道該按鈕是否被點選來採取相應的處理。
    • 在以下示例中,TapboxB通過回撥將其狀態匯出到其父元件,狀態由父元件管理,因此它的父元件為StatefulWidget
  • ParentWidgetState 類:

    • 為TapboxB 管理_active狀態。
    • 實現_handleTapboxChanged(),當盒子被點選時呼叫的方法。
    • 當狀態改變時,呼叫setState()更新UI。
  • 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],
            ),
          ),
        );
      }
    }
    複製程式碼

06.簡單混合管理狀態

  • 對於一些元件來說,混合管理的方式會非常有用。
    • 在這種情況下,元件自身管理一些內部狀態,而父元件管理一些其他外部狀態。
  • 在下面TapboxC示例中
    • 手指按下時,盒子的周圍會出現一個深綠色的邊框,抬起時,邊框消失。點選完成後,盒子的顏色改變。
    • TapboxC將其_active狀態匯出到其父元件中,但在內部管理其_highlight狀態。
    • 這個例子有兩個狀態物件_ParentWidgetState_TapboxCState
  • _ParentWidgetStateC 類:
    • 管理_active 狀態。
    • 實現 _handleTapboxChanged() ,當盒子被點選時呼叫。
    • 當點選盒子並且_active狀態改變時呼叫setState()更新UI。
  • _TapboxCState 物件:
    • 管理_highlight 狀態。
    • GestureDetector監聽所有tap事件。當使用者點下時,它新增高亮(深綠色邊框);當使用者釋放時,會移除高亮。
    • 當按下、抬起、或者取消點選時更新_highlight狀態,呼叫setState()更新UI。
    • 當點選時,將狀態的改變傳遞給父元件。
    //---------------------------- 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,
            ),
          ),
        );
      }
    }
    複製程式碼

07.全域性狀態如何管理

  • 當應用中需要一些跨元件(包括跨路由)的狀態需要同步時,上面介紹的方法便很難勝任了。
    • 比如,我們有一個設定頁,裡面可以設定應用的語言,我們為了讓設定實時生效,我們期望在語言狀態發生改變時,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();
          }
        }
      }
      複製程式碼

08.Provider使用方法

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就會重新生成)

09.訂閱監聽修改狀態

  • 首先定義抽象類。還需要寫上具體的實現類
    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);
    複製程式碼

fluter Utils 工具類庫:github.com/yangchong21…

flutter 混合專案程式碼案例:github.com/yangchong21…

相關文章