Flutter 之 狀態(tài)管理 (八十八)

1. 前言

響應(yīng)式的編程框架中都會有一個永恒的主題——“狀態(tài)(State)管理”,無論是在 React/Vue(兩者都是支持響應(yīng)式編程的 Web 開發(fā)框架)還是 Flutter 中,他們討論的問題和解決的思想都是一致的。

我們想一個問題,StatefulWidget的狀態(tài)應(yīng)該被誰管理?Widget本身?父 Widget ?都會?還是另一個對象?答案是取決于實際情況!

以下是管理狀態(tài)的最常見的方法:

  • Widget 管理自己的狀態(tài)。
  • Widget 管理子 Widget 狀態(tài)。
  • 混合管理(父 Widget 和子 Widget 都管理狀態(tài))。

如何決定使用哪種管理方法?

下面是官方給出的一些原則可以幫助你做決定:

  • 如果狀態(tài)是用戶數(shù)據(jù),如復(fù)選框的選中狀態(tài)、滑塊的位置,則該狀態(tài)最好由父 Widget 管理。
  • 如果狀態(tài)是有關(guān)界面外觀效果的,例如顏色、動畫,那么狀態(tài)最好由 Widget 本身來管理。
  • 如果某一個狀態(tài)是不同 Widget 共享的則最好由它們共同的父 Widget 管理。

在 Widget 內(nèi)部管理狀態(tài)封裝性會好一些,而在父 Widget 中管理會比較靈活。有些時候,如果不確定到底該怎么管理狀態(tài),那么推薦的首選是在父 Widget 中管理(靈活會顯得更重要一些)。

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

image.png

2. 狀態(tài)管理

2.1 Widget管理自身狀態(tài)

_TapboxAState 類:

  • 管理TapboxA的狀態(tài)。
  • 定義_active:確定盒子的當(dāng)前顏色的布爾值。
  • 定義_handleTap()函數(shù),該函數(shù)在點擊該盒子時更新_active,并調(diào)用setState()更新UI。
  • 實現(xiàn)widget的所有交互式行為。

// TapboxA 管理自身狀態(tài)
//------------------------- TapboxA ----------------------------------
class TapBoxA extends StatefulWidget {
  const TapBoxA({Key? key}) : super(key: key);

  @override
  State<TapBoxA> createState() => _TapBoxAState();
}

class _TapBoxAState extends State<TapBoxA> {
  bool _active = false;
  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200,
        height: 200,
        alignment: Alignment.center,
        color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        child: Text(_active ? "Active" : "Inactive",
            style: TextStyle(color: Colors.white)),
      ),
    );
  }
}

134.gif

2.2 父Widget管理子Widget的狀態(tài)

在以下示例中,TapboxB通過回調(diào)將其狀態(tài)導(dǎo)出到其父組件,狀態(tài)由父組件管理,因此它的父組件為StatefulWidget。但是由于TapboxB不管理任何狀態(tài),所以TapboxB為StatelessWidget。

_ParentWidgetState 類:

  • 為TapboxB 管理_active狀態(tài)。
  • 實現(xiàn)_handleTapboxChanged(),當(dāng)盒子被點擊時調(diào)用的方法。
    當(dāng)狀態(tài)改變時,調(diào)用setState()更新UI。

TapboxB 類:

  • 繼承StatelessWidget類,因為所有狀態(tài)都由其父組件處理。
  • 當(dāng)檢測到點擊時,它會通知父組件。

// ParentWidget 為 TapboxB 管理狀態(tài).

//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
  const ParentWidget({Key? key}) : super(key: key);

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;
  void _handleTapboxChanged(bool newValue) {
    _active = newValue;
    setState(() {});
  }

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

//------------------------- TapboxB ----------------------------------
class TapBoxB extends StatelessWidget {
  const TapBoxB({Key? key, this.active = false, required this.onChanged})
      : super(key: key);

  final bool active;

  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        onChanged(!active);
      },
      child: Container(
        width: 200,
        height: 200,
        color: active ? Colors.green[700] : Colors.grey[600],
        alignment: Alignment.center,
        child: Text(active ? "Active" : "Inactive"),
      ),
    );
  }
}

133.gif

2.3 混合狀態(tài)管理

對于一些組件來說,混合管理的方式會非常有用。在這種情況下,組件自身管理一些內(nèi)部狀態(tài),而父組件管理一些其他外部狀態(tài)。

在下面 TapboxC 示例中,手指按下時,盒子的周圍會出現(xiàn)一個深綠色的邊框,抬起時,邊框消失。點擊完成后,盒子的顏色改變。 TapboxC 將其_active狀態(tài)導(dǎo)出到其父組件中,但在內(nèi)部管理其_highlight狀態(tài)。這個例子有兩個狀態(tài)對象_ParentWidgetState和_TapboxCState。

_ParentWidgetStateC類:

  • 管理_active 狀態(tài)。
  • 實現(xiàn) _handleTapboxChanged() ,當(dāng)盒子被點擊時調(diào)用。
    當(dāng)點擊盒子并且_active狀態(tài)改變時調(diào)用setState()更新UI。

_TapboxCState 對象:

  • 管理_highlight 狀態(tài)。
  • GestureDetector監(jiān)聽所有tap事件。當(dāng)用戶點下時,它添加高亮(深綠色邊框);當(dāng)用戶釋放時,會移除高亮。
  • 當(dāng)按下、抬起、或者取消點擊時更新_highlight狀態(tài),調(diào)用setState()更新UI。
  • 當(dāng)點擊時,將狀態(tài)的改變傳遞給父組件。

//---------------------------- ParentWidgetC ----------------------------
class ParentWidgetC extends StatefulWidget {
  const ParentWidgetC({Key? key}) : super(key: key);

  @override
  State<ParentWidgetC> createState() => _ParentWidgetCState();
}

class _ParentWidgetCState extends State<ParentWidgetC> {
  bool _active = false;

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

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

//----------------------------- TapBoxC ------------------------------

class TapBoxC extends StatefulWidget {
  const TapBoxC({Key? key, this.active = false, required this.onChanged})
      : super(key: key);
  final ValueChanged<bool> onChanged;
  final active;
  @override
  State<TapBoxC> createState() => _TapBoxCState();
}

class _TapBoxCState extends State<TapBoxC> {
  bool _highlight = false;

  void _handleTapCancel() {
    _highlight = false;
    setState(() {});
  }

  void _handleTapDown(TapDownDetails details) {
    _highlight = true;
    setState(() {});
  }

  void _handleTapUp(TapUpDetails details) {
    _highlight = false;
    setState(() {});
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      onTapDown: _handleTapDown,
      onTapUp: _handleTapUp,
      child: Container(
        width: 200,
        height: 200,
        decoration: BoxDecoration(
          color: widget.active ? Colors.green[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(color: Colors.teal[700]!, width: 10.0)
              : null,
        ),
        alignment: Alignment.center,
        child: Text(widget.active ? "Active" : "Inactive",
            style: TextStyle(color: Colors.white)),
      ),
    );
  }
}

132.gif

另一種實現(xiàn)可能會將高亮狀態(tài)導(dǎo)出到父組件,但同時保持_active狀態(tài)為內(nèi)部狀態(tài),但如果你要將該TapBox 給其它人使用,可能沒有什么意義。 開發(fā)人員只會關(guān)心該框是否處于 Active 狀態(tài),而不在乎高亮顯示是如何管理的,所以應(yīng)該讓 TapBox 內(nèi)部處理這些細(xì)節(jié)。

3. 全局狀態(tài)管理

當(dāng)應(yīng)用中需要一些跨組件(包括跨路由)的狀態(tài)需要同步時,上面介紹的方法便很難勝任了。比如,我們有一個設(shè)置頁,里面可以設(shè)置應(yīng)用的語言,我們?yōu)榱俗屧O(shè)置實時生效,我們期望在語言狀態(tài)發(fā)生改變時,App中依賴應(yīng)用語言的組件能夠重新 build 一下,但這些依賴應(yīng)用語言的組件和設(shè)置頁并不在一起,所以這種情況用上面的方法很難管理。這時,正確的做法是通過一個全局狀態(tài)管理器來處理這種相距較遠(yuǎn)的組件之間的通信。目前主要有兩種辦法:

    1. 實現(xiàn)一個全局的事件總線,將語言狀態(tài)改變對應(yīng)為一個事件,然后在APP中依賴應(yīng)用語言的組件的initState 方法中訂閱語言改變的事件。當(dāng)用戶在設(shè)置頁切換語言后,我們發(fā)布語言改變事件,而訂閱了此事件的組件就會收到通知,收到通知后調(diào)用setState(...)方法重新build一下自身即可。
    1. 使用一些專門用于狀態(tài)管理的包,如 Provider、Redux

https://book.flutterchina.club/chapter2/state_manage.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容