美文网首页Flutter
Flutter Widget简介

Flutter Widget简介

作者: 沉江小鱼 | 来源:发表于2019-09-26 09:58 被阅读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实际上就是Element的配置数据,Widget树实际上是一个配置数,而真正的UI渲染树是由Element构成;不过,由于Element是通过Widget生成的,所以它们之间有对应关系,在大多数场景,我们可以宽泛地认为Widget树就是指UI控件树或者UI渲染树。
    • 一个Widget对象可以对应多个Element对象。这个很容易理解,根据同一份配置(Widget),可以创建多个实例(Element)。

    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
      该类继承自 DiagnosticableTree,DiagnosticableTree 即“诊断树”,主要作用是提供调试信息。
    • Key
      key用于控制控件如何取代树中的另一个控件.
      如果这两个控件的runtimeType和key属性分别operator==(这里我理解为两个控件的runtimeType和key分别相等,不知道对不对?), 那么新的控件通过更新底层元素来替换旧的控件(通过调用Element.update). 否则旧的控件将从树上删除, element会生成新的控件, 然后新的element会被插入到树中.
      另外,使用GlobalKey作为控件的key,允许element在树周围移动(改变父节点),而不会丢失状态。当找到一个新的Widget时,(它的key和type与同一位置的前一个Widget不匹配),但是在前面的结构中某个地方有一个具有相同全局键的Widget,那么该Widget的元素将被移动到新的位置。
      通常,作为另一个Widget的唯一子节点的Widget不需要一个明确的Key。
    • Element createElement()
      将本身的配置扩展到具体实例(Element)。
      上面说过Widget只是描述Element的配置数据,这个方法就是通过Widget生成一个Element。通过一个Widget可以生成多个Element。
      Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象,这个方法是Flutter Framework隐式调用的,开发时基本不会用到。
    • debugFillProperties()
      复写父类的方法,主要是设置诊断树的一些特性。
    • canUpdate()
      是一个静态方法,它主要用于在Widget树重新build时复用旧的widget,具体来说应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;我们可以在其源码中看到,只要newWidget和oldWidget的runtimeType和key同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element。

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

    • Widget是一个抽象类
    • 最核心的就是定义了 createElement()接口,开发中我们一般不直接继承Widget,而是通过继承StatelessWidget或StatefulWidget来间接继承Widget类。

    StatelessWidget

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

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

    abstract class StatelessWidget extends Widget {
    
      StatelessElement createElement() => StatelessElement(this);
    
      Widget build(BuildContext context);
    }
    
    • StatelessElement createElement() => StatelessElement(this);
      StatelessElement间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。
    • Widget build(BuildContext context);
      这个方法就是需要返回一个新创建的Widget,比如Text,Image,这个Widget由其构造函数和BuildContext的信息配置。给定的BuildContext中包含了关于构建Widget所在树中的位置的信息。
      当这个StatelessWidget插入到给定[BuildContext]树中的时候,或者依赖项发生更改的时候(比如由该小部件引用的[InheritedWidget]发生更改时)会调用此方法。
      这个context,表示当前widget在widget树中的上下文,每个widget都会对应一个context对象(因为每个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置执行“相关操作”的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法,下面是在子树中获取父级widget的一个实例:
    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();
    }
    
    • StatefulElement 间接继承自 Element类,和StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用 createState()来创建状态(State)对象。
    • createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framwork就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

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

    • 在widget构建时可以被同步读取
    • 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其 build() 方法重新构建widget树,从而达到更新UI的目的。

    State中有两个常用属性:

    • widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用声明周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但是State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
    • context,StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。

    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会依次被调用。

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

    • initState
      当widget第一次插入到widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.inheritFromWidgetOfExactType(该方法用于在widget树上获取离当亲widget最近的一个父级InheritFromWidget),原因是在初始化完成后,widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在build()或 didchangeDependencies()中调用用它。
    • didChangeDependencies()
      当State对象的依赖发生变化时会被调用;例如在之前build()中包含了一个InheritedWidget,然后在之后的build()中InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景就是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
    • build()
      主要是用于构建widget子树的,会在如下场景被调用:
      1、在调用initState()之后
      2、在调用didUpdateWidget()之后
      3、在调用setState()之后
      4、在调用didChangeDependencies()之后
      5、在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
    • reassemble
      这个回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在release模式下永远不会被调用
    • didUpdateWidget()
      在widget重新构建时,Flutter framework会调用 Widget.canUpdate 来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate 返回true则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在新旧widget的key和runtimeType同时相等时 didUpdateWidget() 就会被调用。
    • deactivate()
      当state对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插入到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose() 方法。
    • dispose
      当State对象从树中被永久移除时调用;通常在此会调用释放资源。

    StatefulWidget生命周期如图所示:


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

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

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

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

    • 状态访问不便
      如果StatefulWidget有很多状态,每次状态改变都要调用build方法,由于状态是保存在State中的,如果build方法在StatefulWidget中,那么build方法和状态分别在两个类中,那么构建时读取状态将会很不方便。如果真的将build方法放在StatefulWidget中的话,由于构建用户界面过程需要依赖State,所以build方法必须要添加一个State参数,大概是下面这样:
     Widget build(BuildContext context, State state){
          //state.counter
          ...
      }
    

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

    • 继承StatefulWidget不便
      例如,Flutter中有一个动画widget的基类 AnimateWidget,它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法 build(BuildContext context),继承自AnimateWidget的动画widget都要实现这个build方法。现在设想一下,如果Statefulwidget类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个state对象,这就意味着AnimateWidget必须将自己的State对象(记为_animatedWidgetState)提供给子类,因为子类需要在其build方法中调用父类build方法,代码可能如下:
    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对象。

    • 通过Context获取
      context对象有一个 ancestorStateOfType(TypeMatcher) 方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。下面是实现打开SnackBar的实例:
    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
      Flutter还有一种通用的获取State对象的方法:通过GlobalKey来获取,分为两步:
      1、给目标StatefulWidget添加GlobalKey。
    // 定义一个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树中必须是唯一的,不能重复。

    相关文章

      网友评论

        本文标题:Flutter Widget简介

        本文链接:https://www.haomeiwen.com/subject/xrusyctx.html