为你的 Flutter APP 添加交互性

7,052 阅读5分钟

官方文档

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 是一个 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 都是由父部件传递过来的

效果如下:

然后我们来看看输出:

初始化时输出如下:

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 中调用 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: _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 里面执行的。

如有错误,还请指出,谢谢!