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,如圖所示:

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

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

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

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