Flutter

Flutter Widget简介

2019-09-26  本文已影响0人  沉江小鱼

首先,在Flutter中几乎所有的对象都是一个Widget。
跟原生开发中的“控件”不同,Flutter中的Widget的概念更加广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件:用于手势监测的 GestureDetector widget,用于App 主题数据传递的Theme等等。

本文主要是介绍 Widget StatelessWidget 和StatefulWidget,在开发过程中,我们经常通过继承StatelessWidget或者StatefuleWidget自定义Widget。

Widget和Element

在Flutter中,Widget的功能时“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,它只是描述显示元素的一个配置数据。

实际上,Flutter中真正代表屏幕上显示元素的类是 Element,也就是说Widget只是描述 Element的配置数据。
Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每个Element节点都会对应一个Widget对象,总结如下:

Widget 类

abstract class Widget extends DiagnosticableTree{
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

通过查看Widget类的代码,我们可以总结如下:

StatelessWidget

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,过程中会递归的构建其嵌套的Widget。

StatelessWidget比较简单,它继承自Widget类,并且重写了createElement() 方法,我们可以看下:

abstract class StatelessWidget extends Widget {

  StatelessElement createElement() => StatelessElement(this);

  Widget build(BuildContext context);
}
class ContextRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget树中向上查找最近的父级`Scaffold` widget
          Scaffold scaffold = context.ancestorWidgetOfExactType(Scaffold);
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

使用继承StatelessWidget自定义widget的简单的例子:

class Echo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: UIColors.red,
        child: Text("这是一个文本组件"),
      ),
    );
  }
}

StatefulWIdget

和StatelessWidget一样,StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element对象并不相同;另外StatefulWidget类中添加了一个新的方法 createState()。
我们来看下StatefulWidget的类定义:

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}

State
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

State中有两个常用属性:

State生命周期
这里使用一个实例来演示一下State的生命周期,实现一个计数器widget,点击它可以使计数器加1,由于要保存计数器的数值状态,所以我们应该继承StatefulWidget,代码如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

CounterWidget接收一个initValue整型参数,它表示计数器的初始值,下面我们看下State的代码:

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }

}

接下来创建一个新路由,在新路由中,我们只显示一个CounterWidget:

Widget build(BuildContext context) {
  return CounterWidget();
}

我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

可以看到,在StatefulWidget插入到Widget树时首先initState方法会被调用。
然后我们点击⚡️按钮热重载,控制台输出日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

可以看到此时initState 和didChangeDependencies都没有被调用,而此时didUpdateWidget被调用。
接下来,我们在widget树中移除CounterWidget,将路由build方法改为:

Widget build(BuildContext context) {
  //移除计数器 
  //return CounterWidget();
  //随便返回一个Text()
  return Text("xxx");
}

然后热重载,日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

我们可以看到,在CounterWidget从widget树中移除时,deactive和dispose会依次被调用。

下面我们来看看各个回调函数:

StatefulWidget生命周期如图所示:


屏幕快照 2019-09-18 下午5.12.05.png

Tips:在继承StatefulWidget重写其方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中先调用父类方法。

为什么要将build方法放在State中,而不是放在StatefulWidget中?

为什么build方法放在State 而不是StatefulWidget中?这主要是为了提高开发的灵活性。如果将build()方法在StatefulWidget中则会有两个问题:

 Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

这样的话就只能将State的所有状态声明为公开的状态,这样才能在State类外部访问状态。但是,将状态设置为公开后,状态将不再具有私密性,这就会导致对状态的修改将会变得不可控。但如果将build()方法放在State中的话,构建过程不仅可以直接访问状态,而且也无需公开私有状态,这会非常方便。

class MyAnimationWidget extends AnimatedWidget{
    @override
    Widget build(BuildContext context, State state){
      //由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
      //所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState
      //暴露给其子类   
      super.build(context, _animatedWidgetState)
    }
}

这样很显然是不合理的,因为:
1、AimatedWiget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部。
2、如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
综上所述,可以发现,对于StatefulWidget,将build方法放在State中,可以给开发带来很大的灵活性。

在Widget树中获取State对象

由于StatefulWidget的具体逻辑都在其State中,所以很多时候,我们需要获取StatefulWidget对应的State对象来调用一些方法,比如Scaffold组件对应的状态类ScaffoldState中就定义打开SnackBar(路由页底部提示条)的方法,我们有两种方法在子widget树中获取父级StatefulWidget的State对象。

Scaffold(
  appBar: AppBar(
    title: Text("子树中获取State对象"),
  ),
  body: Center(
    child: Builder(builder: (context) {
      return RaisedButton(
        onPressed: () {
          // 查找父级最近的Scaffold对应的ScaffoldState对象
          ScaffoldState _state = context.ancestorStateOfType(
              TypeMatcher<ScaffoldState>());
          //调用ScaffoldState的showSnackBar来弹出SnackBar
          _state.showSnackBar(
            SnackBar(
              content: Text("我是SnackBar"),
            ),
          );
        },
        child: Text("显示SnackBar"),
      );
    }),
  ),
);

一般来说,如果StatefulWidget的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其State对象;如果StatefulWidget的状态是希望暴露出来的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过 context.ancestorStateOfType 获取 StatefulWidget的状态方法是通用的,我们并不能在语法层面指定StatefulWidget的状态是否私有,所以在Flutter开发中便有了一个默认的约定:如果StatefulWidget的状态是希望暴露出来的,应该在StatefulWidget中提供一个of静态方法来获取其State对象,开发者便可直接通过该方法来获取;如果State不希望暴露,则不提供of方法。这个约定在Flutter SDK里随处可见。所以,上面示例中的Scaffold也提供了一个of方法,我们其实可以直接调用它:

...//省略无关代码
// 直接通过of静态方法来获取ScaffoldState 
ScaffoldState _state = Scaffold.of(context); 
_state.showSnackBar(
  SnackBar(
    content: Text("我是SnackBar"),
  ),
);
// 定义一个globalKey,由于GlobalKey要保持全局唯一性,我们使用静态变量存储。
static GlobalKey<ScaffoldState> _globalKey = GlobalKey();
...
Scaffold(
  key: _globalKey, // 设置key
  ...
)

2、通过GlobalKey获取State对象
_globalKey.currentState.openDrawer()

GlobalKey是Flutter提供的一种在整个App中引用element的机制。如果一个widget设置了GlobalKey,那么我们便可以通过globalKey.currentWidget获取该widget对象,globalKey.currentElement来获得widget对应的element对象,如果当前widget是StatefulWidget,则可以通过globalKey.currentState来获得该widget对应的state对象。

注意:使用GlobalKey开销比较大,如果有其他可选方案,应该避免使用它。另外,同一个GlobalKey在整个widget树中必须是唯一的,不能重复。

上一篇下一篇

猜你喜欢

热点阅读