Flutter實(shí)戰(zhàn) 狀態(tài)管理

2021-03-06 17:31 更新

響應(yīng)式的編程框架中都會(huì)有一個(gè)永恒的主題——“狀態(tài)(State)管理”,無(wú)論是在 React/Vue(兩者都是支持響應(yīng)式編程的 Web 開發(fā)框架)還是 Flutter 中,他們討論的問(wèn)題和解決的思想都是一致的。所以,如果你對(duì) React/Vue 的狀態(tài)管理有了解,可以跳過(guò)本節(jié)。言歸正傳,我們想一個(gè)問(wèn)題,StatefulWidget的狀態(tài)應(yīng)該被誰(shuí)管理?Widget 本身?父 Widget?都會(huì)?還是另一個(gè)對(duì)象?答案是取決于實(shí)際情況!以下是管理狀態(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)界面外觀效果的,例如顏色、動(dòng)畫,那么狀態(tài)最好由 Widget 本身來(lái)管理。
  • 如果某一個(gè)狀態(tài)是不同 Widget 共享的則最好由它們共同的父 Widget 管理。

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

接下來(lái),我們將通過(guò)創(chuàng)建三個(gè)簡(jiǎn)單示例 TapboxA、TapboxB 和 TapboxC 來(lái)說(shuō)明管理狀態(tài)的不同方式。 這些例子功能是相似的 ——?jiǎng)?chuàng)建一個(gè)盒子,當(dāng)點(diǎn)擊它時(shí),盒子背景會(huì)在綠色與灰色之間切換。狀態(tài) _active確定顏色:綠色為true ,灰色為false,如圖3-4所示。a large grey box with the text, 'Inactive'

下面的例子將使用GestureDetector來(lái)識(shí)別點(diǎn)擊事件,關(guān)于該GestureDetector的詳細(xì)內(nèi)容我們將在后面“事件處理”一章中介紹。

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

_TapboxAState 類:

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

// TapboxA 管理自身狀態(tài).


//------------------------- 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],
        ),
      ),
    );
  }
}

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

對(duì)于父 Widget 來(lái)說(shuō),管理狀態(tài)并告訴其子 Widget 何時(shí)更新通常是比較好的方式。 例如,IconButton是一個(gè)圖標(biāo)按鈕,但它是一個(gè)無(wú)狀態(tài)的 Widget,因?yàn)槲覀冋J(rèn)為父 Widget 需要知道該按鈕是否被點(diǎn)擊來(lái)采取相應(yīng)的處理。

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

ParentWidgetState 類:

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

TapboxB 類:

  • 繼承StatelessWidget類,因?yàn)樗袪顟B(tài)都由其父組件處理。
  • 當(dāng)檢測(cè)到點(diǎn)擊時(shí),它會(huì)通知父組件。

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


//------------------------ 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],
        ),
      ),
    );
  }
}

#3.2.3 混合狀態(tài)管理

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

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

_ParentWidgetStateC類:

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

_TapboxCState 對(duì)象:

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

//---------------------------- 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) {
    // 在按下時(shí)添加綠色邊框,當(dāng)抬起時(shí),取消高亮  
    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,
        ),
      ),
    );
  }
}

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

#3.2.4 全局狀態(tài)管理

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

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

本書將在"功能型組件"一章中介紹 Provider 包的實(shí)現(xiàn)原理及用法,同時(shí)也將會(huì)在"事件處理與通知"一章中實(shí)現(xiàn)一個(gè)全局事件總線,讀者有需要可以直接翻看。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)