对于Native开发的同学刚接触flutter来说,编写代码比较困恼或者说不习惯的地方主要有两个:
-
由命令式编程到声明式编程的思维转变困恼,编写代码时各种别扭
tips:命令式编程和声明式编程的概念和区别就自行Google了
-
编写代码时,一层层嵌套,陷入嵌套地狱的困恼,严重影响编码体验、代码可读性和后期的维护
命令式编程到声明式编程的思维转变就只能靠自己慢慢适应了,但嵌套地狱,是有方法来优化的。
宠物列表 栗子
我们用一个简单的栗子来循序渐进,如何利用语言特性来逐步优化,提升编码的体验,增强代码的可读性,以及后期的可维护性。
一个简单的需求如下,显示一个宠物的列表,效果如如下:

虽然是一个很简单的栗子,但按原始的写法,我们的代码是这个样子的:
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Nested Demo'),
),
body: Container(
child: Offstage(
offstage: false,
child: ListView(
children: <Widget>[
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('喵星人'),
],
),
),
Divider(
height: 2,
),
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('汪星人'),
],
),
),
Divider(
height: 2,
),
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('呜星人'),
],
),
),
Divider(
height: 2,
),
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('嗷星人'),
],
),
),
Divider(
height: 2,
),
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('哞星人'),
],
),
),
Divider(
height: 2,
),
Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text('咯星人'),
],
),
),
],
),
),
),
);
}
}
对于从Native转过来接触Flutter的同学来说,这,简直了,就一个嵌套地狱呀...
从代码来看,创建每个显示的item和分割线,都是重复代码,于是自然就会想到抽取重复代码。
拆分嵌套的代码
而当前应对嵌套过深的方式,主流做法,就是对widget进行拆分,把一个大的build方法,拆分为小的widget创建方法。
我们将item的创建和分割线的创建,抽取为私有方法,得到如下代码:
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Nested Demo'),
),
body: Container(
child: Offstage(
offstage: false,
child: ListView(
children: <Widget>[
_buildItem("喵星人"),
_bulidDivider(),
_buildItem("汪星人"),
_bulidDivider(),
_buildItem("呜星人"),
_bulidDivider(),
_buildItem("嗷星人"),
_bulidDivider(),
_buildItem("哞星人"),
_bulidDivider(),
_buildItem("咯星人"),
],
),
),
),
);
}
Container _buildItem(String name) {
return Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text(name),
],
),
);
}
Widget _bulidDivider() {
return Divider(
height: 2,
);
}
}
咋看下来,代码结构和嵌套层级都得到优化,一般对嵌套的优化也就到这一步了。
看_buildItem这个方法,相对来说,嵌套层级是不是还是有点深,看起来不是那么舒适,我们就只能做到这一步吗?
想想,要不把创建Row也抽取一个方法,不过真的有这个必要吗,如果是再复杂一点的需求呢,会拆成茫茫多的方法,到时候优化了嵌套地狱,同时又掉进了维护火葬场。
自定义扩展 —— extension
对于写代码来说,如果在嵌套和.语法之前做选择,相信更多的人都愿意使用.语法,易用也好理解。
以上述的_buildItem方法为例:
Container _buildItem(String name) {
return Container(
color: Colors.white,
padding: EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text(name),
],
),
);
}
最外层的Container,是否可以使用.语法来进行添加呢?
Dart在2.6.0时支持了extension语法,使.语法来优化嵌套成为了可能。
所以我们来利用这个extension扩展一下widget,添加一个Container嵌套:
// 单个widget的扩展常用Widget
extension SingleWidgetNestedCommonlyUsedExtension on Widget {
// 嵌套一个container
Container nestedContainer({
Key key,
AlignmentGeometry alignment,
EdgeInsetsGeometry padding,
Color color,
Decoration decoration,
Decoration foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
EdgeInsetsGeometry margin,
Matrix4 transform,
Clip clipBehavior = Clip.none,
}) {
return Container(
key: key,
alignment: alignment,
padding: padding,
color: color,
decoration: decoration,
foregroundDecoration: foregroundDecoration,
width: width,
height: height,
constraints: constraints,
margin: margin,
transform: transform,
clipBehavior: clipBehavior,
child: this,
);
}
}
写完该扩展了之后,所有的widget都可以.一个nestedContainer(...)函数,函数的参数与Container的构造函数一致,那么_buildItem方法,就可以写成这样:
Container _buildItem(String name) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text(name),
],
).nestedContainer(
color: Colors.white,
padding: EdgeInsets.all(20),
);
}
可以通过.语法来添加层级嵌套,大大减少嵌套地狱的可能,也提升了编程体验。
既然Container可以通过extension来变为链式调用,那么其他的单子widget的widget就都可以使用该方式来添加,如下是我封装的
单个widget的扩展,包含如下四个子扩展:
- 单个widget的扩展常用Widget

- 单个widget的扩展非常用Widget

- 单个widget手势嵌套扩展

- 单个widget转list扩展

通过上述扩展,我们的代码就可以写成这样子:
class SPShopVisitRecordBottomButtonWidget extends StatelessWidget {
final VoidCallback onTaped;
SPShopVisitRecordBottomButtonWidget({Key key, this.onTaped})
: super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: 0.5,
color: CRMColor.separator,
),
ClipRRect(
borderRadius: BorderRadius.circular(17),
child: Container(
color: CRMColor.primary,
child: _createFlatButton(),
))
.nestedContainer(
height: 34,
width: 200,
)
.nestedCenter()
.nestedContainer(
height: 49.5,
)
],
).nestedContainer(
color: Colors.white,
height: 50,
);
}
/// 创建按钮
Widget _createFlatButton() {
return Text(
'新增拜访',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
).nestedFlatButton(
shape: RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(17))),
onPressed: onTaped);
}
}
多子Widget扩展
比如Row、Colum、ListView...等,这些多子Widget的,我们同样希望能够使用.语法来进行链式调用,于是添加了如下扩展:
- 多个widget 添加widget 扩展
// 多个widget 添加widget 扩展
extension MultipleWidgetAddWidgetExtension<T extends Widget> on List<T> {
// 添加一个 widget
List<Widget> addWidget(Widget widget,
{AddWidgetAsListType addType = AddWidgetAsListType.behind}) {
if (addType == AddWidgetAsListType.front) {
return [widget] + this;
}
return this..add(widget);
}
// 添加 widget 数组 然后 返回一个list
List<Widget> addWidgetList(List<Widget> widgets,
{AddWidgetAsListType addType = AddWidgetAsListType.behind}) {
if (addType == AddWidgetAsListType.front) {
return widgets + this;
}
return this + widgets;
}
}
- List<T>转List<Widget>的扩展方法,其实用List的map也可以,为了方便,扩展了这个方法
/// widget 嵌套 的list 扩展
extension WidgetNestedListExtension<T> on List<T> {
// 将list转换为widget数组
List<Widget> transformToWidgets(Widget Function(T) builder) {
return this.map<Widget>((item) {
return builder(item);
}).toList();
}
}
- 多个widget嵌套扩展
// 多个widget嵌套扩展
extension MultipleWidgetNestedExtension<T extends Widget> on List<T> {
// 嵌套 listView
ListView nestedListView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
double itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
}) {
return ListView(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
itemExtent: itemExtent,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
children: this,
);
}
// 添加其他多子Widget的扩展,Row、Colum、CustomScrollView、GridView、Stack
使用添加好的扩展优化上述栗子
添加好这些扩展之后,我们的代码可以写成这样:
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Nested Demo'),
),
body: buildContainer(),
);
}
Container buildContainer() {
// 链式调用
return [
_buildItem("喵星人"),
_bulidDivider(),
_buildItem("汪星人"),
_bulidDivider(),
_buildItem("呜星人"),
_bulidDivider(),
_buildItem("嗷星人"),
_bulidDivider(),
_buildItem("哞星人"),
_bulidDivider(),
_buildItem("咯星人"),
].nestedListView().nestedOffstage(offstage: false).nestedContainer();
}
Container _buildItem(String name) {
// 完全链式调用
return [
Icon(Icons.pets),
SizedBox(
width: 12,
),
Text(name)
]
.nestedRow(crossAxisAlignment: CrossAxisAlignment.center)
.nestedContainer(color: Colors.green, padding: EdgeInsets.all(20));
}
Widget _bulidDivider() {
return Divider(
height: 2,
);
}
}
更加链式的封装
为了让我们的代码更加符合链式编程风格,再定义一个静态方法
// widget嵌套用widget
class WidgetNestedWidget {
static Widget addWidget(Widget widget) {
return widget;
}
}
完全使用链式调用,最后的代码是这个样子的:
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Nested Demo'),
),
body: _createBody());
}
// 纯链式
Widget _createBody() {
return [
"喵星人",
'line',
"汪星人",
'line',
"呜星人",
'line',
"嗷星人",
'line',
"哞星人",
'line',
"咯星人"
]
.transformToWidgets((title) {
if (title == 'line') {
return Divider(
height: 2,
);
}
return WidgetNestedWidget.addWidget(Icon(Icons.pets))
.addWidgetListAsList([
SizedBox(
width: 12,
),
Text(title)
])
.nestedRow(crossAxisAlignment: CrossAxisAlignment.center)
.nestedContainer(
color: Colors.purple, padding: EdgeInsets.all(20));
})
.nestedListView()
.nestedOffstage(offstage: false)
.nestedContainer();
}
}
扩展函数与嵌套的混合调用
虽然我们扩展了常用的Widget为链式调用,但是链式和嵌套是可以共存的,它们是可以交叉使用的,并非非此即彼的关系,如下,保留关键部分的嵌套,其他部分使用链式:
/// 创建头部
Widget _createHeader(
ServiceVisitRecordState state, Dispatch dispatch, ViewService viewService) {
return state.showEmptyWidget
? Container()
: Row(
children: <Widget>[
RichText(
text: [
TextSpan(
text: '${state.totalCount}',
style: TextStyle(
color: CRMColor.error,
),
),
TextSpan(text: '个小记'),
].nestedTextSpan(
text: '按条件共筛选出',
style: TextStyle(color: CRMColor.desc, fontSize: 12),
),
),
],
).nestedContainer(
color: CRMColor.background,
height: 32,
padding: EdgeInsets.only(left: 12),
);
}
一些注意点
上述内容对对代码结构和编码体验带来提升,在扩展和使用时,有几个注意点也需要注意一下:
- 对TextSpan扩展,TextSpan也是多子Widget,但是要去子Widget必须继承自InlineSpan,所以扩展应该是这么写:
// TextSpan 嵌套扩展
extension MultipleWidgetNestedTextSpan<T extends InlineSpan> on List<T> {
// 嵌套一个 TextSpan
TextSpan nestedTextSpan({
String text,
TextStyle style,
GestureRecognizer recognizer,
String semanticsLabel,
}) {
return TextSpan(
text: text,
style: style,
recognizer: recognizer,
semanticsLabel: semanticsLabel,
children: this);
}
}
- 从上述优化的最终版本来看,虽然我们完全使用扩展函数进行链式调用,但是代码结构其实也被破坏了
所以个人建议,扩展函数的封装是用来解决嵌套过深的问题的,使用扩展函数的同时,需要保留部分关键嵌套层级结构以使得布局的层级结构保持清晰,以免带来代码阅读和维护的困难,扩展函数与嵌套混用,具体使用到什么程度,个人觉得是不要带来麻烦的程度,具体就看个人的选择了。
后记
上述的测试代码和完整的封装,传送门
网友评论