響應(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 內(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所示。
下面的例子將使用GestureDetector
來(lái)識(shí)別點(diǎn)擊事件,關(guān)于該GestureDetector
的詳細(xì)內(nèi)容我們將在后面“事件處理”一章中介紹。
_TapboxAState 類:
_active
:確定盒子的當(dāng)前顏色的布爾值。_handleTap()
函數(shù),該函數(shù)在點(diǎn)擊該盒子時(shí)更新_active
,并調(diào)用setState()
更新UI。// 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],
),
),
);
}
}
對(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),所以TapboxB
為StatelessWidget
。
ParentWidgetState
類:
_active
狀態(tài)。_handleTapboxChanged()
,當(dāng)盒子被點(diǎn)擊時(shí)調(diào)用的方法。setState()
更新 UI。TapboxB 類:
StatelessWidget
類,因?yàn)樗袪顟B(tài)都由其父組件處理。// 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],
),
),
);
}
}
對(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)。_handleTapboxChanged()
,當(dāng)盒子被點(diǎn)擊時(shí)調(diào)用。_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ì)移除高亮。_highlight
狀態(tài),調(diào)用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 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é)。
當(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)的組件之間的通信。目前主要有兩種辦法:
initState
方法中訂閱語(yǔ)言改變的事件。當(dāng)用戶在設(shè)置頁(yè)切換語(yǔ)言后,我們發(fā)布語(yǔ)言改變事件,而訂閱了此事件的組件就會(huì)收到通知,收到通知后調(diào)用setState(...)
方法重新build
一下自身即可。本書將在"功能型組件"一章中介紹 Provider 包的實(shí)現(xiàn)原理及用法,同時(shí)也將會(huì)在"事件處理與通知"一章中實(shí)現(xiàn)一個(gè)全局事件總線,讀者有需要可以直接翻看。
更多建議: