Stateful & stateless
在 Flutter 中, Widget
分為兩種,一種是有狀態的稱為 StatefulWidget
,一種是無狀態的稱為 StatelessWidget
。例如 Checkbox
核取方塊是 StatefulWidget
(具有可變狀態的小部件),而 Text
部件就是 StatelessWidget
(不需要可變狀態的小部件)。
我們自定義 StatefulWidget
的時候,會重寫 createState()
方法來建立一個 State
物件,在 State
中可以通過 setState((){...});
方法來改變當前 Widget
的狀態,而 StatelessWidget
則不可以。
建立一個 StatefulWidget
例項的方法如下:
class FavoriteWidget extends StatefulWidget {
// 建立 State 物件
@override
State<StatefulWidget> createState() {
return _FavoriteWidgetState();
}
}
// 用來管理 FavoriteWidget 的狀態
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
void _toggleFavorite() {
// 改變狀態
setState(() {
_isFavorited = !_isFavorited
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
// 這裡可以根據 _isFavorited 去改變改變部分 widget 的狀態
...
);
}
}
複製程式碼
在 Dart 中,成員變數或者類名稱以 下劃線 開頭表示該成員或者類為 private 的。 官方文件
管理 Widget 的狀態
Flutter提供了一下幾種方式管理 widget 的狀態
widget
狀態由自己管理- 由父
widget
管理widget
的狀態 - 混合管理
widget
的狀態 (自己和父部件各管理一部分)
widget 狀態由自己管理
這個比較簡單,直接在 widget
內部管理自己的狀態
// 有可變狀態的 widget
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key) {
print('TapboxA init');
}
// 建立 State 用來管理當前 widget 的狀態
@override
_TapboxAState createState() => _TapboxAState();
}
// State
class _TapboxAState extends State<TapboxA> {
_TapboxAState() {
print('boxA state init');
}
bool _active = false;
void _handleTap() {
print('boxA state handleTap is call');
// 設定狀態
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
print('boxA state build is call');
return GestureDetector(
onTap: _handleTap, // 點選事件
child: Container(
child: Center(
// 根據 _active 來設定不同的 Text 值
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[600],
),
),
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: TapboxA(),
),
),
);
}
}
複製程式碼
效果如下:
初始化順序:
flutter: TapboxA init
flutter: boxA state init
flutter: boxA state build is call
複製程式碼
點選事件被觸發時:
flutter: boxA state handleTap is call
flutter: boxA state build is call
複製程式碼
由父 widget 管理
案例中的 TapboxB
是一個 StatelessWidget
, ParentWidget
是一個 StatefulWidget
。
我們需要使用 ParentWidget
來改變 TapboxB
的狀態。
//------------------------- parent widget ----------------------------------
class ParentWidget extends StatefulWidget {
ParentWidget() {
print('ParentWidget init');
}
@override
State<StatefulWidget> createState() {
return _ParentWidgetState();
}
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
_ParentWidgetState() {
print('ParentWidgetState init');
}
void _handleTapboxChanged(bool newValue) {
print('parent _handleTapboxChanged is call : $newValue');
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
print('ParentWidgetState build is call');
return MaterialApp(
title: 'ParentWidget',
theme: ThemeData(
primaryColor: Colors.redAccent
),
home: Scaffold(
appBar: AppBar(
title: Text('ParentWidget'),
),
body: Center(
child: TapboxB(onChanged: _handleTapboxChanged, active: _active,),
),
),
);
}
}
//------------------------- child widget ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key) {
print('Tap boxB init : ${this.active}');
}
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
print('child _handleTap : $active');
onChanged(!active);
}
@override
Widget build(BuildContext context) {
print('Tap boxB build method');
return GestureDetector(
onTap: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600]
),
child: Center(
child: Text(active ? 'Android' : 'Flutter',
style: TextStyle(fontSize: 32.0, color: Colors.white),),
),
),
);
}
}
複製程式碼
可以看到,TapboxB
的構造方法中有一個 onChanged
引數,和 active
引數;onChanged
引數型別為 ValueChanged<bool>
他是一個接受一個引數的回撥方法,方法原始碼如下:
/// Signature for callbacks that report that an underlying value has changed.
///
/// See also [ValueSetter].
typedef void ValueChanged<T>(T value);
複製程式碼
在 TapboxB
中,當觸發點選事件的時候就是執行這個回撥方法,也就是上述程式碼中 _handleTap()
方法中所執行的語句。而 active
用來切換當前 TapboxB
的狀態。
注意:onChanged 和 active 都是由父部件傳遞過來的
效果如下:
然後我們來看看輸出:
初始化時輸出如下:
Performing hot restart...
flutter: ParentWidget init
Restarted app in 1,976ms.
flutter: ParentWidgetState init
flutter: ParentWidgetState build is call
flutter: Tap boxB init : false
flutter: Tap boxB build method
複製程式碼
執行順序如下:
我們再看看子部件(TapboxB)觸發點選事件時的輸出
flutter: child _handleTap : false
flutter: parent _handleTapboxChanged is call : true
flutter: ParentWidgetState build is call
flutter: Tap boxB init : true
flutter: Tap boxB build method
複製程式碼
執行順序如下:
說明:當我們觸發子部件上的點選事件時候,這個時候會執行 _handleTap()
方法,_handleTap()
方法裡面會執行 onChanged(...)
,接著就會執行父部件裡面的回撥方法 _handleTapboxChanged(...)
,注意 _handleTapboxChanged(...)
方法裡面執行了 setState(() {...})
,在這個方法裡面切換了狀態,然後會重新呼叫 build
方法重新渲染子部件。
setState(() {...})
會使 widget 重繪,類似於 Android 中呼叫 View 的invalidate()
方法
混合管理 widget 的狀態
混合管理就是某些狀態由自己管理,某些狀態由父部件來管理。
下面的例子就是一個混合管理狀態的例子,部件 TabboxC
在被點選時有三個狀態變換,背景色,文字和邊框。
示例中,背景色和文字的狀態交由父部件來管理(和上一個示例類似),而邊框狀態由自己管理。
既然父部件和子部件都能管理狀態,那麼它們都是要繼承StatefulWidget
類。
// ------------parent widget-----------
class ParentWidget2 extends StatefulWidget {
ParentWidget2() {
print('Parent init');
}
@override
State<StatefulWidget> createState() {
return _ParentWidgetState2();
}
}
class _ParentWidgetState2 extends State<ParentWidget2> {
_ParentWidgetState2() {
print('_Parent State init');
}
bool _active = false;
void _handleTapboxChanged(bool newValue) {
print('_Parent _handleTapboxChanged method is called');
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
print('_Parent State build is called');
return TabboxC(onChanged: _handleTapboxChanged, active: _active,);
}
}
// ------------child widget-----------
class TabboxC extends StatefulWidget {
// 構造方法
TabboxC({
Key key,
this.active: false,
@required this.onChanged
}) : super(key: key) {
print('TabboxC init');
}
final bool active;
final ValueChanged<bool> onChanged;
@override
State<StatefulWidget> createState() {
return _TapboxCState();
}
}
class _TapboxCState extends State<TabboxC> {
bool _highlight = false;
_TapboxCState() {
print('_TapboxC State init');
}
void _handleTapDown(TapDownDetails details) {
print('_TapboxC tap down');
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
print('_TapboxC tap up');
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
print('_TapboxC tap cancel');
setState(() {
_highlight = false;
});
}
void _handleTap() {
print('_TapboxC tap clicked');
widget.onChanged(!widget.active);
}
@override
Widget build(BuildContext context) {
print('_TapboxCState build is called');
return MaterialApp(
title: 'mix',
theme: ThemeData(
primaryColor: Colors.redAccent
),
home: Scaffold(
appBar: AppBar(
title: Text('mix'),
),
body: Center(
child: GestureDetector(
// down
onTapDown: _handleTapDown,
// up
onTapUp: _handleTapUp,
// cancel
onTapCancel: _handleTapCancel,
// click
onTap: _handleTap,
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
// Box 顏色 父控制元件 控制(通過回撥方法)
color: widget.active ? Colors.lightGreen[700] : Colors
.grey[600],
// 邊框顏色 自己控制
border: _highlight ? Border.all(
color: Colors.teal[700], width: 10.0) : null
),
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),),
),
),
),
)
),
);
}
}
複製程式碼
效果如下:
初始化時候的順序和上面類似,我們來看看點選事件被觸發時候的執行順序:
flutter: _TapboxC tap down
flutter: _TapboxCState build is call
flutter: _TapboxC tap up
flutter: _TapboxC tap clicked
flutter: _Parent _handleTapboxChanged method is call
flutter: _Parent State build is call
flutter: TabboxC init
flutter: _TapboxCState build is call
複製程式碼
執行流程如下:
大家可能會發現,子部件在 Down 事件中呼叫了
setState(...)
方法,然後執行了一次 build 操作;而在 Up 事件中同樣也呼叫了setState(...)
方法,但是為什麼沒有執行 build 操作,而是直接執行了 click 操作。這裡面可能和 Android 裡面類似,在 View 的 onTouchEvent 方法裡面,onClick 方法也是在 ACTION_UP 裡面執行的。
如有錯誤,還請指出,謝謝!