众所周知Flutter中一切皆为Widget,并且Widget之间还存在着嵌套组合的艺术,每个Widget可以功能单一,又可以将不同的Widget相互组合形成另一个Widget。然而这些Widget组合的结果就是,Weiget个数会成几何倍数增长,现在Widget个数怎么说也有500了吧。(这个是官方对Widget做的分类Widget Catalog)。
那么怎么合理的组合Widget形成另一个Widget,官方的Container就是一个很好的例子,他很好的将LimitedBox、ConstrainedBox、Align、Padding、ClipPath、DecoratedBox、Transform等Widget组合到了一起,这里可以看下源码感受一下。
无独有偶,实际上官方确实也组合了很多像Container一样好用的Widget。但是,也是因为Widget的可组合性太强了,同一个实现常常会有很多种的组合的方式。这就形成大家很喜欢自己造一些轮子的趋势,虽然也可以完成,但会出现很多相似的Widget。这样后期难以维护,所以建议大家优先使用官方的一些封装好的Widget。当然也有可能是刚入门,渐进式的使用Flutter,对Widget没有一个大概的了解,就不知不觉的走了弯路。
我呢开始也走了不少弯路,一边享受着Widget可以无限嵌套封装的快感,一边感到不知道到底应该选用哪个Widget的困惑。随着慢慢的了解,也渐渐的总结出了不同场景下,应该使用哪些Widget。
在这里把它分享出来,希望大家一起学习,一起探讨。以下都是我自己的主观认知,如有不对,还望指正。
详细可点击链接在官网查看对应Widget的文档以及演示(部分Widget已经有详细演示)
功能 | 优选 | 不推荐 | 描述 |
---|---|---|---|
容器 | Container | LimitedBox、ConstrainedBox、Align、Padding、ClipPath、DecoratedBox、Transform | 功能这么多的Container用起来它不香吗,但是如果你只使用了一个属性,例如外边距,还是建议直接使用Padding。 |
容器动画 | AnimatedContainer | AnimatedXxxx | 每个属性都有单独的以Animated为前缀的隐式动画实现,但如果想让child使用动画请考虑使用AnimatedSwitcher。 |
容器内容居中 | 设置Alignment.center | Center | Container中不要再套用Center了,直接设置Alignment为center即可。 |
比例容器 | AspectRatio | 手动计算宽或高 | 比列尽可能写成 2 / 3,而不是0.6667,不仅精度高而且易读。扩展:GridView中item的Size就是通过,副轴长度固定,然后按照比例算出主轴长度,来实现固定布局的。 |
阴影 | PhysicalModel、Card | BoxShadow | 有多种实现方式,但是PhysicalModel术业有专工。 |
卡片效果 | Card | Material | Card的效果其实是使用Material进行了上层封装,想要实现卡片效果,直接使用Card即可。skr~ |
圆形头像 | CircleAvatar、ClipOval | ClipRRect | ClipRRect更适用于圆角,它的圆形只是一种特殊情况。ClipRRect虽是椭圆,但更常用它的圆形表达形式。CircleAvatar看到名字就不用太多解释了。 |
局部刷新 | ValueNotifier & ValueListenableBuilder | 自封装StatefulWidget | 多留意后缀是Builder的组件,有些功能其实官方已经有了封装,这等小事就不要再去造轮子了。 |
布局刷新 | LayoutBuilder | 👆 可用于Web页面做自适应布局。 | |
方向更改 | OrientationBuilder | LayoutBuilder | 👆旋转设备方向改变触发布局更新。注意: 并非是设备方向,只是长和宽发生了互换,导致原先定义的正方向失效了。 此Widget依赖于LayoutBuilder,只是更关注于前面所说的方向。 |
单次异步 | FutureBuilder | 👆 异步只能执行一次。 | |
多次异步流 | StreamBuilder | 👆 可以向数据流中添加多次值,每次接收到数据后便刷新布局。 | |
自定义动画 | AnimatedBuilder | AnimatedWidget | 👆 用于构建自定义动画,可以将动画Widget中不需要变化的部分放到child节点,优化动画性能。 |
自定义插间动画 | TweenAnimationBuilder | AnimatedWidget | 👆 使用与AnimatedBuilder类似,但其只是double类型的插间,而TweenAnimationBuilder可以指定插间类型,例如:Color、Offset、Rect等。 |
旋转 | RotatedBox | Transform.rotate | Transform旋转前后占用的空间不会变化:[一日] => [丨日];RotatedBox是先旋转然后应用布局,所以不存在旋转框占用布局:[一日] => [旧] |
画布中旋转 | RotationTransition | Canvas.rotate在Canvas中旋转 | 可以在CustomPaint的画布上尽情作画,然后将旋转应用到这个Widget上,而不是驱动画布旋转,每次都计算角度偏移再重画一遍。 |
分割线 | Divider | Container、Border | 快收起你的奇技淫巧,分割线不用这么麻烦,Divider就够了。如果你想用网格线可以使用GridPaper, 但它限定主线宽1pixel。想自定义线宽的话用这个GridPaperExp |
透明度 | Color().withOpacity() | Opacity | 这里主要想说应用透明度,Opacity不是最优选的方式,它有一定的性能成本,可以在这里查看官方解释。 |
按钮 | ElevatedButton、TextButton、OutlinedButton、IconButton、CupertinoButton | GestureDetector、InkWell、Listener | 大多数情况下你如果只需要一个点击事件,那以上Widget都可以满足需求,但相对来说体验上会差一些。这时候尽可以使用官方提供好的XxxxButton,这样按下动画、禁用显示灰色等特性就无须再去实现了。 |
手势 | GestureDetector | RawGestureDetector、Listener | Listener + GestureRecognizer = RawGestureDetector;RawGestureDetector + Common GestureRecognizer = GestureDetector 一般使用GestureDetector,如果预定义的手势不能满足需求,可以直接操作屏幕上监听的点使用Listener,或把这些点抽象成手势,实现对应的手势识别器,赋于RawGestureDetector这就形成了自定义手势。 |
Item固定长度列表 | ListView(itemExtent: 22.22,)、SliverFixedExtentList | SliverList | 既然知道了每一项的长度,就不要让容器动态去计算了,这在无限长度的布局中,可以提高不少性能。ListView如果没有设置itemExtent,最终实现就是SliverList,反之就是SliverFixedExtentList。 |
固定扩展无限布局 | SingleChildScrollView | ListView | 如果布局比较固定,只是出现了一些不稳定因子,例如折叠伸缩文字,布局必须可变才能抵消其影响,这时候推荐使用此Widget。而只有在明确布局为无限长度时才推荐使用ListView。[补充] |
主题 | Theme | 没有含义且多次重复的数值、颜色 | 这些数据比较分散,集中取值便于管理,更便于修改。界面的风格统一比内容更重要。 |
弹性布局 | Flex、Column、Row | 我们有必要先看一下,他们之前的关系结构,然后再去考虑选择使用哪一个Widget。关系] | |
填充弹性布局 | Flexible、Expanded、Spacer | 👆 | |
待续 |
// 👍
Container(
color: Colors.purple,
alignment: Alignment.center,
child: Text(text),
)
// 🙅♂️
Container(
color: Colors.blue,
child: Center(
child: Text(text),
),
)
// 👍
PhysicalModel(
color: Colors.grey,
elevation: 10.0,
shadowColor: Colors.grey[900],
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: child,
)
// 👍
Card(
color: Colors.grey,
elevation: 10.0,
shadowColor: Colors.grey[900],
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: child,
)
// 🙅♂️
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: borderRadius,
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.grey,
offset: Offset(2.0, 8.0),
blurRadius: 10.0,
)
],
),
child: child,
),
可以看到SingleChildScrollView在再次折叠文字后变为固定布局,页面便不可以滚动。而在成为滚动布局后,行为与ListView基本一致,比如Android溢出有水波纹,iOS溢出弹簧性显示。
_child = Column(children: _children);
// 👍
SingleChildScrollView(
child: _child,
),
// 🙅♂️
ListView(
children: _children,
)
// 🙅♂️
_child
class Row extends Flex {
Row({
Key? key,
...
List<Widget> children = const <Widget>[],
}) : super(
children: children,
key: key,
direction: Axis.horizontal, // 这里确定了水平方向
...
);
}
// 看了Row,那闭着眼睛就知道Column肯定设置了Axis.vertical垂直方向。
所以当布局水平、垂直显示方向不确定时,例如手机由竖屏切换到横屏,原本的Column就变成了Row,这时候就应该使用Flex,然后根据当前方向指定direction属性。
class Expanded extends Flexible {
const Expanded({
Key? key,
int flex = 1,
required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child); // 这里设定了FlexFit.tight紧约束
}
//----------------------------------------------
class Spacer extends StatelessWidget {
const Spacer({Key? key, this.flex = 1}) : super(key: key);
final int flex;
@override
Widget build(BuildContext context) {
return Expanded( // 这里直接返回了包了一个最小SizedBox的Expanded。
flex: flex,
child: const SizedBox.shrink(),
);
}
}
//----------------------------------------------
const SizedBox.shrink({ Key? key, Widget? child })
: width = 0.0,
height = 0.0,
super(key: key, child: child);
//----------------------------------------------
enum MainAxisAlignment {
start,
end,
center,
// 以下是三种用来填充空白的方式
spaceBetween,
spaceAround,
spaceEvenly,
}
所以如果子节点想要使用紧约束(紧约束这里可以理解为child会被强制占满整个空白,松约束就是child可以在零和最大约束之间使用自己的大小,不会被强制占满空白),就使用Expanded。想要随时调整约束就用Flexible。
如果你只是想填充弹性布局中固定子节点之外剩余的空白,可以直接使用已经封装好的弹性空白组件Spacer。但或许你可以直接使用MainAxisAlignment来组织空白空间去控制固定子节点的排列方式。