前言
一個永恆的主題,“狀態(State)管理”,無論是在React/Vue(兩者都是支持響應式編程的Web開發框架)還是Flutter中,他們討論的問題和解決的思想都是一致的。
一個問題,StatefulWidget的狀態應該被誰管理?Widget本身?父Widget?都會?還是另一個對象?答案是取決於實際情況!以下是管理狀態的最常見的方法:
- Widget管理自己的狀態。
- Widget管理子Widget狀態。
- 混合管理(父Widget和子Widget都管理狀態)。
如何決定使用哪種管理方法?下面是官方給出的一些原則:
- 如果狀態是用戶數據,如復選框的選中狀態、滑塊的位置,則該狀態最好由父Widget管理。
- 如果狀態是有關界面外觀效果的,例如顏色、動畫,那么狀態最好由Widget本身來管理。
- 如果某一個狀態是不同Widget共享的則最好由它們共同的父Widget管理。
三個例子
下面通過三個例子來分別說明這三種管理方式。
Widget管理自身狀態
_TapBoxAState 類:
- TapBoxA。
- 定義_active:確定盒子的當前顏色的布爾值。
- 定義_handleTap()函數,該函數在點擊該盒子時更新_active,並調用setState()更新UI。
- 實現widget的所有交互式行為。
class TapBoxA extends StatefulWidget {
TapBoxA({Key key}) : super(key: key);
@override
_TapBoxAState createState() => _TapBoxAState();
}
class _TapBoxAState extends State<TapBoxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
// 使用GestureDetector來識別點擊事件
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[700],
),
),
);
}
}
父Widget管理子Widget的狀態
對於父Widget來說,管理狀態並告訴其子Widget何時更新通常是比較好的方式。
例如,IconButton是一個圖標按鈕,但它是一個無狀態的Widget,因為我們認為父Widget需要知道該按鈕是否被點擊來采取相應的處理。
ParentWidgetState 類:
- 為TapBoxB 管理_active狀態。
- 實現_handleTaPBoxChanged(),當盒子被點擊時調用的方法。
- 當狀態改變時,調用setState()更新UI。
TapBoxB 類: - 繼承StatelessWidget類,因為所有狀態都由其父組件處理。
- 當檢測到點擊時,它會通知父組件。
TaPBoxB通過回調將其狀態導出到其父組件,狀態由父組件管理,因此它的父組件為StatefulWidget。
但是由於TapBoxB不管理任何狀態,所以TapBoxB為StatelessWidget。
class ParentWidgetB extends StatefulWidget {
@override
_ParentWidgetBState createState() => _ParentWidgetBState();
}
class _ParentWidgetBState extends State<ParentWidgetB> {
bool _active = false;
void _handleTaPBoxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapBoxB(
active: _active,
onChanged: _handleTaPBoxChanged,
),
);
}
}
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);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[400] : Colors.grey[400],
),
),
);
}
}
混合狀態管理
TapBoxC示例中,手指按下時,盒子的周圍會出現一個深綠色的邊框,抬起時,邊框消失。
點擊完成后,盒子的顏色改變。TapBoxC將其_active狀態導出到其父組件中,但在內部管理其_highlight狀態。
這個例子有兩個狀態對象_ParentWidgetState和_TaPBoxCState。
_ParentWidgetStateC類:
- 管理_active 狀態。
- 實現 _handleTaPBoxChanged() ,當盒子被點擊時調用。
- 當點擊盒子並且_active狀態改變時調用setState()更新UI。
_TapBoxCState 對象:
- 管理_highlight 狀態。
- GestureDetector監聽所有tap事件。當用戶點下時,它添加高亮(深綠色邊框);當用戶釋放時,會移除高亮。
- 當按下、抬起、或者取消點擊時更新_highlight狀態,調用setState()更新UI。
- 當點擊時,將狀態的改變傳遞給父組件。
class ParentWidgetC extends StatefulWidget {
@override
_ParentWidgetCState 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(active: _active, onChanged: _handleTapBoxChanged,),
);
}
}
class TapBoxC extends StatefulWidget {
TapBoxC({Key key, this.active: false, @required this.onChanged}): super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
_TapBoxCState createState() => _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 GestureDetector(
onTapDown: _handleTapDown, // 處理按下事件
onTapUp: _handleTapUp, // 處理抬起事件
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(
widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[200] : Colors.grey[200],
border: _highlight ? Border.all(color: Colors.teal[700], width: 10.0,) : null,
),
),
);
}
}
全局狀態管理
當應用中需要一些跨組件(包括跨路由)的狀態需要同步時,上面三種管理方式就很難勝任了。
有一個設置頁,里面可以設置應用的語言,為了讓設置實時生效,期望在語言狀態發生改變時,APP中依賴應用語言的組件能夠重新build一下,但這些依賴應用語言的組件和設置頁並不在一起。
而全局狀態管理則可以處理這種相距較遠的組件之間的通信,目前有兩種方法:
- 實現一個全局的事件總線,將語言狀態改變為一個事件,然后在App中依賴應用語言的組件的initState方法中訂閱語言改變的事件。
當用戶設置頁切換語言后,發布語言改變事件,而訂閱了此事件的組件就會收到通知,收到通知后調用setState()重新build一下即可。 - 使用一些專門用於狀態管理的包,如Provider、Redux等。