初略講解Flutter的狀態管理

zane發表於2019-07-15

響應式的程式設計框架中都會有一個永恆的主題——“狀態管理”,無論是React/Vue(兩者都是支援響應式程式設計的Web開發框架)還是Flutter,討論的問題和解決的思想都是一致的。所以,如果你對React/Vue的狀態管理有了解,可以跳過本節。言歸正傳,我們想一個問題,Stateful Widget的狀態應該被誰管理?是Widget本身?是父Widget?還是都會?亦或是另一個物件?答案是:取決於實際情況!

以下是管理狀態的最常見的方式:

  • Widget管理自身的狀態;
  • 父Widget管理子Widget的狀態;
  • 混合管理(父Widget和子Widget都管理狀態)。

以下原則可以幫助你決定如何決定使用哪種管理方式?

  • 如果狀態是使用者資料,如核取方塊的選中狀態、滑塊的位置,則該狀態最好由父Widget來管理;
  • 如果狀態是有關介面外觀效果的,如顏色、動畫,則該狀態最好由Widget本身來管理;
  • 如果某一個狀態是不同的Widget共享的,則最好由它們共同的父Widget來管理。

在Widget內部管理狀態封裝性會好一些,而在父Widget中管理會比較靈活。有些時候,如果不確定到底該由誰來管理狀態,那麼首選由父Widget來管理(因為靈活會顯得更重要一些)。

接下來,我們將通過建立三個簡單示例TapboxA、TapboxB和TapboxC來說明管理狀態的不同方式。這些例子的功能是相似的——建立一個盒子,當點選它時,盒子背景會在綠色與灰色之間切換,狀態_active確定顏色:綠色為true,灰色為false

初略講解Flutter的狀態管理初略講解Flutter的狀態管理

下面的例子將使用GestureDetector來識別點選事件,關於該GestureDetector的詳細內容我們將在後續進行講解。

方式一: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],
        ),
      ),
    );
  }
}
複製程式碼

方式二:父Widget管理子Widget的狀態

對於父Widget來說,管理狀態並告訴其子Widget何時更新通常是比較好的方式。例如,IconButton是一個圖片按鈕,但它是一個無狀態的Widget,因為我們認為父Widget需要知道該按鈕是否被點選從而採取相應的處理。

在以下示例中,TapboxB通過回撥將其狀態匯出到其父項。由於TapboxB不管理任何狀態,因此它的父類為StatefulWidget,TapboxB為StatelessWidget。

ParentWidgetState類:

  • 為TapboxB管理_active狀態;
  • 實現_handleTapboxChanged(),當盒子被點選時呼叫的方法;
  • 當狀態改變時,呼叫setState()更新UI。

TapboxB類:

  • 繼承StatelessWidget類,因為所有狀態都由其父Widget處理;
  • 當檢測到點選時,它會通知父Widget。
// ParentWidget 為 TapboxB 管理狀態.

//------------------------ ParentWidget --------------------------------

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 Container(
      child: new 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 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],
        ),
      ),
    );
  }
}
複製程式碼

方式三:混合管理

對於一些Widget來說,混合管理的方式非常有用,在這種情況下,Widget自身管理一些內部狀態,而父Widget管理一些其它外部狀態。

在下面TapboxC示例中,按下時,盒子的周圍會出現一個深綠色的邊框,抬起時,邊框消失,點選生效,盒子的顏色改變。TapboxC將其_active狀態匯出到其父Widget中,但在內部管理其_highlight狀態。這個例子有兩個狀態物件_ParentWidgetCState_TapboxCState

_ParentWidgetCState物件:

  • 管理_active狀態;
  • 實現_handleTapboxChanged(),當盒子被點選時呼叫;
  • 當點選盒子並且_active狀態改變時呼叫setState()方法更新UI。

_TapboxCState物件:

  • 管理_highlight狀態;
  • GestureDetector監聽所有Tap事件,當使用者按下時,新增高亮(深綠色邊框),當使用者抬起(釋放按下)時,會移除高亮;
  • 當按下、抬起或者取消點選時更新_highlight狀態,呼叫setState()方法更新UI;
  • 當按下時,將狀態的改變傳遞給父Widget。
//---------------------------- 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 Container(
      child: 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,
        ),
      ),
    );
  }
}
複製程式碼

擴充套件:全域性狀態管理

當應用中包括一些跨Widget(甚至跨路由)的狀態需要同步時,上面介紹的方法就很難勝任了。比如,有一個設定頁,裡面可以設定應用語言,但是為了讓設定實時生效,我們期望在語言狀態發生改變時,APP Widget能夠重新構建(build),但是APP Widget和設定頁並不在一起,那怎麼辦呢?正確的做法是通過一個全域性狀態管理器來處理這種“相距較遠”的Widget之間的通訊。

目前只要有兩種方法:

  1. 實現一個全域性的事件匯流排,將語言狀態改變對應為一個事件,然後在APP Widget所在的父Widget initState()方法中訂閱語言狀態改變的事件,當使用者在設定頁切換語言狀態後,觸發語言狀態改變事件,然後APP Widget就會收到通知,接著重新構建(build)一下即可。
  2. 使用redux的全域性狀態包,讀者可以在pub上檢視其詳細資訊。

關於全域性事件匯流排的實現,我們後續進行。

相關文章