Flutter实战:Flutter中的两个StateWidget及常用UI控件

这篇文章旨在带领读者从对Flutter的布局方式找不着北到基本分辨出东南西北,至于导航到具体到哪一点该怎么走就得看你们的悟性了哇哈哈哈哈哈哈。

StatelessWidget和StatefulWidget

StatelessWidget和StatefulWidget是Flutter中比较重要的两个控件父类。
StatelessWidget是无状态控件,在构建的时候就已经决定了最终形态。
StatefulWidget是有状态控件,在构建的时候不是它最终的形态,而是可以随着绑定的数据等变更而在生命周期内进行变化。

简单对比StatelessWidget与StatefulWidget初始化及数据改变流程如下表所示

StatelessWidget StatefulWidget
接收外部传入的数据,执行控件build方法,只有在外部传入的数据不同的时候UI才会显示不一样 接收外部传入的数据,执行控件build方法和状态初始化方法,当传入的数据不同或者绑定的数据改变时UI才会重新渲染

StatelessWidget

因为StatelessWidget是无状态的控件,所以只需要知道它在初始化的时候经过了createElementbuild的过程即可。

StatefulWidget

StatefulWidget的生命周期比较复杂,当一个StatefulWidget页面打开的时候,经历的过程是这样的:

  1. StatefulWidget控件的构造方法
  2. createState
  3. StatefulWidget绑定的State的构造方法
  4. initState
  5. didChangeDependencies
  6. build

下面是每个方法的解释:

StatefulWidget控件构造方法:在外部push进来或者直接指定widget的情况下调用。

createState:当Flutter框架在创建一个StatefulWidget控件的时候,会立即调用createState方法,这个方法返回一个State具体实现对象,用于处理StatefulWidget控件生命周期中的状态事件。

StatefulWidget绑定的State的构造方法:在createState中new的时候调用。

initState:当StatefulWidget控件创建后(插入渲染树的时候)即调用这个方法,一半会在这个方法里面初始化一些变量,调起异步网络请求获取数据,创建对BuildContext依赖的成员。

didChangeDependencies:当调用initState后会立即调用这个方法,这个方法是在State对象被创建好了但没有准备好构建(build)的时候调用的。

build:调用这个方法来构建widget。

build方法调用后,控件就会显示在渲染树上,屏幕就能看到你所定义的控件的内容。

另外,StatefulWidget还有更新状态和被移除、或者通过WidgetsBinding注册观察者来扩展得到类似Android原生变成中onResume、onPause等生命周期,这部分这里不展开讲解,对于接下来的内容可在不了解这部分内容情况下学习,感兴趣的读者可自行上Google百度一下了解。

Flutter中常用的控件

在Flutter中对控件的划分是通过子节点(child,你要理解为孩子也行)的个数,这里选取在实际项目中使用到的几个解释一下,并且在最后给出在实战项目中一个具体布局来做详细讲解。更多的可以以此类推。

单子节点布局(child):Container、Expanded、Padding
多子节点布局(children):Row、Column、Stack

Container

查看源码可以知道Container继承子StatelessWidget,它的构造方法参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Container({
Key key,
this.alignment,
this.padding,
Color color,
Decoration decoration,
this.foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
})

对每个参数的解释如下:

参数名 意义
key 用于控制一个widget替换另一个widget的标识符
alignment 对其方式
padding 内边距
color 背景颜色
decoration 装饰器,将会绘制在子节点底层
foregroundDecoration 装饰器,将会绘制在子节点顶层
width
height
constrainnts 对child的约束
margin 外边距
transform container的变换矩阵
child 子节点控件

Expanded

看名字就知道这是一个用于“扩展”的控件,它的源码构造方法如下:

1
2
3
4
5
const Expanded({
Key key,
int flex = 1,
@required Widget child,
})

对每个参数的解释如下:

参数名 意义
key 用于控制一个widget替换另一个widget的标识符
flex 是否依照子节点占位灵活调整占用空间大小
child 子节点控件

其中Expanded是继承Flexible,只是少了一个fit参数,Expanded将fit固定为FlexFit.tight,关于这个参数及相关联的参数解释如下:

  • FlexFit.tight:强制子组件填充可用空间,不受子节点width属性控制。
  • FlexFit.loose:子节点最多能充满屏幕空间,受子节点width属性控制。

所以要实现同样的功能也可以使用Flexible并指定FlexFit.tight即可。

Padding

一个可以定义内边距的控件,按我们的习惯控件的padding可能直接当一个属性处理,但在flutter里面,padding也封装为了一个控件,构造方法源码如下:

1
2
3
4
5
const Padding({
Key key,
@required this.padding,
Widget child,
})

各个参数的意义如下:

参数名 意义
key 用于控制一个widget替换另一个widget的标识符
padding 一个EdgeInsetsGeometry对象,定义了各个边
child 子节点控件

Row & Column

水平布局控件,可以理解为在一行内横向排布控件。
要注意的是在水平布局控件里面,有主轴和交叉轴的概念,横向为主轴,纵向为交叉轴。
构造方法源码如下:

1
2
3
4
5
6
7
8
9
10
Row({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List<Widget> children = const <Widget>[],
})

各个参数的意义如下:

参数名 意义
key 用于控制一个widget替换另一个widget的标识符
mainAxisAlignment 主轴上的对齐方式,默认是start
mainAxisSize 主轴的尺寸,默认是max,即最大化主轴方向可用空间
crossAxisAlignment 交叉轴上的对齐方式,默认是center
textDirection 子节点在主轴的方向,默认从左到右
verticalDirection 子节点在交叉轴的方向,默认从上到下
textBaseline 文字基线
children 子节点集合

至于Column,与Row基本一样的,只是在垂直方向布局,这里就不说了。

Stack

Stack类似Android中的FrameLayout,可以理解为层布局、帧布局、绝对布局。
Stack的构造方法源码如下:

1
2
3
4
5
6
7
8
Stack({
Key key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})

各个参数的意义如下:

参数名 意义
key 用于控制一个widget替换另一个widget的标识符
alignment 对齐方式,默认左上角对齐
textDirection 子节点排列方向
fit 子节点尺寸适配方式,默认loose,表示子节点可以从0到最大子节点的取值
overflow 超出范围的是否裁剪,默认是裁剪
children 子节点集合

实战

前面提到的各个控件都是作为预备知识,下面开始一个项目中实际应用。

依旧是前面《日报》的项目,以一项列表项为例:

UI示意图

我们画出这个列表项的大纲图如下所示:
大纲图

他对应的树形结构如下所示:

树形结构

为每个列表项的widget创建写了一个方法_buildListItem,它的源码对应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Widget _buildListItem(BuildContext buildContext, int index) {
News newsModel = _newsList[index];
return new Container(
height: 100.0,
child: new Card(
child: new Row(
children: <Widget>[
new Container(
margin: new EdgeInsets.all(5.0),
child: new FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: newsModel.image,
fit: BoxFit.cover,
height: 90.0,
width: 90.0,
),
),
new Expanded(
child: new Padding(
padding: new EdgeInsets.fromLTRB(0, 5.0, 5.0, 5.0),
child: new Text(
newsModel.title,
style: new TextStyle(
fontSize: 18.0, fontWeight: FontWeight.bold),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
)
],
),
),
);

逐步完成上述控件

上面是一个总体,下面来一层层分析这些控件的编码,不习惯的可能还会看的一脸懵逼,特别是刚入坑Flutter的,之前写Android用xml或者写iOS用xib甚至手写的。

外层Container

按照结构图,我们完成这部分内容:

外层Container结构

这个很简单,源码如下:

1
2
3
new Container(
height: 100.0,
);

我们定义了这个Container的区域为宽max(不指定即为占位填充可用区域),高100.0。

外层Container里面的CardView

那么在外层添加一个CardView结构如下:

CardView结构

实现如下:

1
2
3
4
new Container(
height: 100.0,
child: new Card(),
);

读者可能就有点纳闷了,明明没有指定外边距啊,为什么结构图里面会标注出来外边距(实际上也是有外边距),是因为CardView默认就指定来一个外边距,我们翻源码可以看到CardView的构造方法是这样的:

1
2
3
4
5
6
7
8
9
10
const Card({
Key key,
this.color,
this.elevation,
this.shape,
this.margin = const EdgeInsets.all(4.0),//注意看这里指定来上下左右都margin为4
this.clipBehavior = Clip.none,
this.child,
this.semanticContainer = true,
}) : super(key: key);

内层Container及里面的FadeInImage

内层Container,或者叫缩略图的外层Container,结构如下:

内层Container结构

实现代码如下:

1
2
3
4
5
6
7
8
9
10
new Container(
margin: new EdgeInsets.all(5.0),
child: new FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: newsModel.image,
fit: BoxFit.cover,
height: 90.0,
width: 90.0,
),
),

这里用到了一个FadeInImage图片控件,用到了memoryNetwork命名构造方法。这是一个渐进式加载图片控件,读者可以跟前面控件介绍的翻源码阅读的方式去了解这个widget。

Expanded及其子节点

在完成上面布局之后,下面就到了仅剩的新闻标题的布局了,结构如下:

Expanded及其子节点

这个(不包括左边蓝色区域内容,画出来知识作对比)实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
new Expanded(
child: new Padding(
padding: new EdgeInsets.fromLTRB(0, 5.0, 5.0, 5.0),
child: new Text(
newsModel.title,
style: new TextStyle(
fontSize: 18.0, fontWeight: FontWeight.bold),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
)

同样对于Text、TextStyle控件读者可以自行翻阅源码了解每个属性。

总结

看到这应该对于Flutter控件的布局方式有大体了解了。
接触Flutter后体会到一点:你看到的一切皆为widget。
尽可能不受客户端编程思想影响,特别是你认为一个属性就可以解决的你正在用的控件为什么没有这个属性,这个时候就应该去找找有没有什么widget来解决问题。
基本上需要的效果都可以通过一层层widget实现,实现方式也是各种各样的。
但不是widget层数越多越好,能少尽量少,毕竟层数多会带来更多的渲染时间,对性能是不友好的。

文章作者: Kevin Wu
文章链接: https://kevinwu.cn/p/81462c08/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 KevinWu.CN
支付宝打赏