為你的 Flutter APP 新增互動性

劉斯龍發表於2018-08-01

官方文件

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 APP 新增互動性

初始化順序:

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 是一個 StatelessWidgetParentWidget 是一個 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 都是由父部件傳遞過來的

效果如下:

為你的 Flutter APP 新增互動性

然後我們來看看輸出:

初始化時輸出如下:

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
複製程式碼

執行順序如下:

為你的 Flutter APP 新增互動性

我們再看看子部件(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
複製程式碼

執行順序如下:

為你的 Flutter APP 新增互動性

說明:當我們觸發子部件上的點選事件時候,這個時候會執行 _handleTap() 方法,_handleTap() 方法裡面會執行 onChanged(...),接著就會執行父部件裡面的回撥方法 _handleTapboxChanged(...),注意 _handleTapboxChanged(...) 方法裡面執行了 setState(() {...}),在這個方法裡面切換了狀態,然後會重新呼叫 build 方法重新渲染子部件。

setState(() {...}) 會使 widget 重繪,類似於 Android 中呼叫 Viewinvalidate() 方法

混合管理 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 APP 新增互動性

初始化時候的順序和上面類似,我們來看看點選事件被觸發時候的執行順序:

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
複製程式碼

執行流程如下:

為你的 Flutter APP 新增互動性

大家可能會發現,子部件在 Down 事件中呼叫了 setState(...) 方法,然後執行了一次 build 操作;而在 Up 事件中同樣也呼叫了 setState(...) 方法,但是為什麼沒有執行 build 操作,而是直接執行了 click 操作。這裡面可能和 Android 裡面類似,在 View 的 onTouchEvent 方法裡面,onClick 方法也是在 ACTION_UP 裡面執行的。

如有錯誤,還請指出,謝謝!

相關文章