響應式的程式設計框架中都會有一個永恆的主題——“狀態管理”,無論是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
。
下面的例子將使用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之間的通訊。
目前只要有兩種方法:
- 實現一個全域性的事件匯流排,將語言狀態改變對應為一個事件,然後在APP Widget所在的父Widget
initState()
方法中訂閱語言狀態改變的事件,當使用者在設定頁切換語言狀態後,觸發語言狀態改變事件,然後APP Widget就會收到通知,接著重新構建(build
)一下即可。 - 使用redux的全域性狀態包,讀者可以在pub上檢視其詳細資訊。
關於全域性事件匯流排的實現,我們後續進行。