Flutter Widget源码解析及实战

1,677 阅读8分钟

Widget

在flutter中所有页面展示出来的元素都是由一个个的widget组成,与原生android开发不同的地方在于flutter中widget不仅仅表示UI元素,他也可以是一个完全和UI无关如GestureDetector,GestureDetector继承自StatelessWidget

Widget的功能类似于原生android开发中的style文件,用来描述UI的样式,最终真正绘制在屏幕上的是Element

@immutable
abstract class Widget extends DiagnosticableTree { 
// DiagnosticableTree 诊断树主要作用是提供调试信息
  
  const Widget({ this.key }); // key 

  // 最终整个绘制流程在Element中进行
  @protected
  Element createElement();

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

  // 提供一些调试用的信息
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  // 这里通过判断key和runtimeType是否一致来判断是否可以更新UI
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

StatelessWidget

无状态的widget一般用于一些静态UI的绘制(例如:Text)或者提供与UI无关的功能(例如:GestureDetector用来管理手势相关的功能),源码如下:

abstract class StatelessWidget extends Widget {
  
  const StatelessWidget({ Key key }) : super(key: key);
  
  @override
  StatelessElement createElement() => StatelessElement(this);
  
  @protected
  Widget build(BuildContext context);
 
 }

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

class TestLess extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text("test"),
    );
  }
}

StatefulWidget

可变状态的小部件

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key key }) : super(key: key);

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

  @protected
  State createState();
}

与StatelessWidget不同的是StatefulWidget类中添加了一个新的接口createState(),一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态。 下面是StatefulWidget的最佳实践:

  • 尽量将需要该表状态的widget防止在子节点,这样在改变整个渲染树的时候就只需要更新一个widget即可,如果将其防止在父节点那么将会导致当前节点的整个子节点的widget都会被重新渲染。

  • 尽量减少build方法中返回的widget的嵌套层级,理想情况下一个StatefulWidget仅仅只包含一个类型为RenderObjectWidget的子widget。例如:RichText,但显然这是不切实际的,但一个小部件越是接近这个理想,效率越高。

  • 如果子树没有更改,请缓存表示该子树的窗口小部件,并在每次使用时重新使用它。对于要重新使用的窗口小部件,要比创建新的(但配置相同的)窗口小部件更有效。将有状态部分分解为带有子参数的小部件是执行此操作的常用方法。

  • 尽可能使用const小部件。 (这相当于缓存窗口小部件并重新使用它。)

  • 避免更改任何创建的子树的深度或更改子树中任何窗口小部件的类型。例如,不是返回包含在[IgnorePointer]中的子项或子项,而是始终将子窗口小部件包装在[IgnorePointer]中并控制[IgnorePointer.ignoring]属性。这是因为更改子树的深度需要重建,布局和绘制整个子树,而只更改属性将需要对渲染树进行尽可能少的更改(例如,在[IgnorePointer]的情况下,没有布局)或重绘是必要的)。

  • 如果由于某种原因必须更改深度,请考虑将子树的公共部分包装在具有[GlobalKey]的小部件中,该[GlobalKey]在有状态小部件的生命周期内保持一致。 (如果没有其他小部件可以方便地分配密钥,[KeyedSubtree]小部件可能对此有用。)

下面是一个名为YellowBird的有状态小部件子类的框架。在这个例子中[State]没有实际状态。State通常表示为私人成员字段。此外,通常小部件有更多的构造函数参数,每个参数都应该为final类型。

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

   @override
   _YellowBirdState createState() => _YellowBirdState();
 }

 class _YellowBirdState extends State<YellowBird> {
   @override
   Widget build(BuildContext context) {
     return Container(color: const Color(0xFFFFE306));
   }
 }

下面的例子显示了更通用的小部件Bird,它可以被赋予一种颜色和一个子widget,并且它有一些内部状态,可以调用一个方法来改变它

class Bird extends StatefulWidget {
   const Bird({
     Key key,
     this.color = const Color(0xFFFFE306),
     this.child,
   }) : super(key: key);

   final Color color;
   final Widget child;

   _BirdState createState() => _BirdState();
 }

 class _BirdState extends State<Bird> {
   double _size = 1.0;

   void grow() {
     setState(() { _size += 0.1; });
   }

   @override
   Widget build(BuildContext context) {
     return Container(
       color: widget.color,
       transform: Matrix4.diagonal3Values(_size, _size, 1.0),
       child: widget.child,
     );
   }
 }

按照惯例,窗口小部件构造函数仅使用命名参数。可以使用[@required]将命名参数标记为必需。按照惯例,第一个参数是[key],最后一个参数是childchildren

State中有两个常用属性

  • widget

表示与State实例相关联的widget实例

  • BuildContext

构建widget的上下文

生命周期

  • initState

当Widget第一次插入到Widget树时会被调用,对于每一个State对象

framework将在创建的每个[State]对象调用此方法一次。重写此方法以执行初始化,该初始化取决于此对象插入树中的位置(即[context])或用于配置此对象的窗口小部件(即[widget])

如果[State]的[build]方法依赖于一个本身可以改变状态的对象,例如[ChangeNotifier]或[Stream],或者一个可以订阅接收通知的其他对象,那么一定要订阅并在[initState],[didUpdateWidget]和[dispose]中取消订阅:

您不能使用此方法中的[BuildContext.inheritFromWidgetOfExactType]。但是,[didChangeDependencies]将在此方法之后立即调用,可以在那里使用[BuildContext.inheritFromWidgetOfExactType]。

  • didChangeDependencies

当State对象的依赖发生变化时会被调用

如果父Widget重建并请求树中的此位置更新以显示具有相同[runtimeType]和[Widget.key]的新Widget,则框架将更新此[State]对象的[widget]属性以引用新Widget然后使用上一个Widget作为参数调用此方法。

覆写此方法可以在[widget]更改时进行响应(例如,开始隐式动画)。

在调用[didUpdateWidget]之后,框架总是调用[build],这意味着对[didUpdateWidget]中的[setState]的任何调用都是多余的。

  • build

它主要是用于构建Widget子树

  • reassemble

此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用

  • didUpdateWidget

在widget重新构建时,framework会调用canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新

  • deactivate

当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法

  • dispose

当State对象从树中被永久移除时调用;通常在此回调中释放资源。

布局类组件相关(原文)

布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排版(layout)方式不同。Element树是通过Widget树来创建的(通过Widget.createElement()),Widget其实就是Element的配置数据。在Flutter中,根据Widget是否需要包含子节点将Widget分为了三类,分别对应三种Element,如下表:

Widget Element 用途
LeafRenderObjectWidget LeafRenderObjectElement Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Text、Image。
SingleChildRenderObjectWidget SingleChildRenderObjectElement 包含一个子Widget,如:ConstrainedBox、DecoratedBox等
MultiChildRenderObjectWidget MultiChildRenderObjectElement 包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等

StatelessWidget和StatefulWidget就是两个用于组合Widget的基类,它们本身并不关联最终的渲染对象(RenderObjectWidget)

最终渲染操作是在build()方法中构建真正的RenderObjectWidget,如Text,它其实是继承自StatelessWidget,然后在build()方法中通过RichText来构建其子树,而RichText才是继承自LeafRenderObjectWidget

实战

具体效果如下:

相关源码如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      physics: AlwaysScrollableScrollPhysics(),
      shrinkWrap: true,
      children: <Widget>[
        _swipe(),
        _checkWidget(),
      ],
    );
  }

  // 头部轮播控件相关代码
  Widget _swipe() {
    return Container(
      height: 180, // 设置轮播控件高度
      child: Swiper( // 轮播控件
        itemBuilder: (BuildContext context, int index) {
          return InkWell(
            onTap: () {},
            child: Image.network(
              "https://sr.aihuishou.com/cms/image/63689137818430153041559824.png",
              fit: BoxFit.fill,
            ),
          );
        },
        itemCount: 3,
        pagination: SwiperPagination( // 轮播控件底部数字
            alignment: Alignment.bottomRight,
            builder: FractionPaginationBuilder(
                fontSize: 20,
                color: Colors.white,
                activeFontSize: 20,
                activeColor: Colors.white)),
        autoplay: true,
      ),
    );
  }

  // 审核管理相关
  Widget _checkWidget() {
    return Container(
      margin: EdgeInsets.only(left: 10, right: 10),
      child: Column( // 纵向布局
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(top: 20), 设置上边距
            child: Row( // 横向布局
              crossAxisAlignment: CrossAxisAlignment.center,
              // 主轴对其方式,再此处为横向布局方式
              mainAxisAlignment: MainAxisAlignment.spaceBetween, 
              children: <Widget>[
                Text(
                  "审核管理",
                  style: TextStyle(color: Colors.black, fontSize: 16),
                ),
                Row(
                  children: <Widget>[
                    Text(
                      "全部",
                      style: TextStyle(color: Colors.black54, fontSize: 12),
                    ),
                    Icon(
                      Icons.keyboard_arrow_right,
                      color: Colors.grey,
                    )
                  ],
                )
              ],
            ),
          ),
          Container(
            margin: EdgeInsets.only(top: 10, bottom: 10),
            padding: EdgeInsets.fromLTRB(10, 15, 10, 15),
            decoration: BoxDecoration( // 审核管理下面的白色边框设置
              color: Colors.white,
              borderRadius: BorderRadius.all(
                Radius.circular(5), // 设置边框圆角
              ),
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              mainAxisSize: MainAxisSize.max,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                // 审核中
                Expanded(
                  flex: 1, // 设置权重为1
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: <Widget>[
                      Container(
                        child: Stack(
                          children: <Widget>[
                            Container(
                              child: Icon(
                                Icons.book,
                                color: Colors.redAccent[100],
                                size: 42,
                              ),
                              width: 35,
                              height: 40,
                              alignment: Alignment.center,
                              decoration: BoxDecoration(
                                  borderRadius: BorderRadius.circular(50)),
                              margin: EdgeInsets.only(top: 2, bottom: 2),
                            ),
                            Positioned(
                                right: 0,
                                top: 0,
                                child: Container(
                                  padding: EdgeInsets.fromLTRB(3, 1, 3, 1),
                                  child: Text(
                                    "4",
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 10,
                                    ),
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.redAccent,
                                    borderRadius: BorderRadius.circular(20),
                                  ),
                                ))
                          ],
                        ),
                        color: Colors.black12,
                      ),
                      Container(
                        margin: EdgeInsets.only(left: 10),
                        child: Text(
                          "审核中",
                          style: TextStyle(
                              color: Colors.black87,
                              fontSize: 14,
                              fontWeight: FontWeight.bold),
                        ),
                      ),
                    ],
                  ),
                ),
                // 审核完成
                Expanded(
                  flex: 1, // 设置权重为1
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: <Widget>[
                      Container(
                        child: Stack(
                          children: <Widget>[
                            Container(
                              child: Icon(
                                Icons.book,
                                color: Colors.redAccent[100],
                                size: 42,
                              ),
                              width: 35,
                              height: 40,
                              alignment: Alignment.center,
                              decoration: BoxDecoration(
                                  borderRadius: BorderRadius.circular(50)),
                              margin: EdgeInsets.only(top: 4, bottom: 4),
                            ),
                            Positioned(
                                right: 10,
                                top: 0,
                                child: Container(
                                  padding: EdgeInsets.fromLTRB(3, 1, 3, 1),
                                  child: Text(
                                    "2",
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 10,
                                    ),
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.redAccent,
                                    borderRadius: BorderRadius.circular(20),
                                  ),
                                ))
                          ],
                        ),
                        width: 50,
                      ),
                      Container(
                        margin: EdgeInsets.only(left: 10),
                        child: Text(
                          "审核完成",
                          style: TextStyle(
                              color: Colors.black87,
                              fontSize: 14,
                              fontWeight: FontWeight.bold),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }