Flutter 避免代码嵌套,写好 build 方法 | 开发者说·DTalk
优化效果 (缩略图):
距离我接触 Flutter 已经过去了九个月,在 Flutter
代码编写的过程中,很多开发者都遇到了 "回调地狱" 的问题。在 Flutter 中,称之为回调并不准确,准确的说,是因为众多 Widget
互相嵌套在一起,导致反括号部分堆积严重,极度影响代码可读性。
本文将介绍一种代码编写风格,最大限度减少嵌套对代码阅读的影响。
初步介绍
Flutter
的 UI 代码:
使用 build 方法
Flutter
的 Widget
使用 build
方法来创建 UI 组件,然后通过注入 child
属性的方式为组件添加子组件,子组件可以继续包含 child
,通过调用每一个 child
的 build
方法,就形成了类似 DOM 结构的组件树,然后由渲染引擎渲染图形。
一个常见的定义组件的例子如下:
class DeleteText extends StatelessWidget {
// 我们在build方法中渲染自定义Widget
Widget build(BuildContext context) {
return Text('Delete');
}
}
要在 Flutter
中定义 (继承) 一个 Widget
,则它的属性必须都是 final
的。final
意味着属性必须在构造函数中就被初始化完成,不接受提前定义,也不接受更改。所以,在生命周期中动态的改变 Widget
对象的属性是不可能的,必须使用框架的 build
方法来为构造函数动态指定参数,从而达到改变组件属性的功能。
class Avatar extends StatelessWidget {
// 如果url属性不是final的,编译器会报出警告
final String url;
// 这个构造方法很长,但是主要你写了final属性,VSCode就会帮我们自动生成
const Avatar({Key key, this.url}) : super(key: key);
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Image.network(url),
);
}
}
Tips: 自动创建构造方法,只要是构造方法没有的 final 属性,点击 "快速修复",就可以自动生成构造方法。
Flutter 语法与 HTML/CSS
嵌套正是 DOM 树的特点,正如 HTML
其实也会无限嵌套一样 (大多数前端可能看 HTML 看习惯了,都忘了 HTML 其实也经常会写成嵌套很深的形式),Flutter
的 UI 代码嵌套本质是不可避免的,这正是 Flutter
UI 代码的编写特点——一次成型,而不是通过 addView
之类的方法来手动管理每一个视图的生命周期。在此基础上,Flutter
可以高效的反复重建 Widget
,在渲染效率上展现出了非常大的优势。
<!-- html的嵌套其实也很深 -->
<div>
<div>
<div>
<div>
<article>
<h1></h1>
<li></li>
</article>
</div>
</div>
</div>
</div>
嵌套代码难以阅读
当我们评判一串代码的时候,一个显而易见的点,就是代码距离左边的距离,如果一行代码距离左边达到了十多个 tab,可想而知它被嵌套在了多么深的位置。
来看看这个 Widget
, 这个 Widget
很简单,左边有一个正文和一个附属文本,附属文本在正文下方,右边有一组按钮,代表这一行的操作,我们再给他嵌套一个动画的渐现效果,处理好字体。那么他的代码应该如下所示:
一个简单的嵌套的情况
class ActionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: 1,
duration: Duration(milliseconds: 800),
child: Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
超级长的左边距 */Text(
'Title',
style: TextStyle(fontSize: 16),
),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
'Desc',
style: TextStyle(fontSize: 12),
),
),
],
),
),
),
Row(
children: <Widget>[
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
child: Text('Edit'),
超级长的左边距 */onPressed: () {
Edit');
},
),
),
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: () {
Delete');
往下数,足足11个反括号
),
),
],
)
],
),
),
);
}
}
Flutter
的开发者一定不会陌生,它可以完美运行,但是十分难以阅读。反括号的数量经常会达到一个更夸张的级别,导致部分内容被顶到过于右边,在阅读时造成了非常大的困难。
解决方法
Dart2
已经可以完全不写 new
了,但有的开发者还在写 new
。去掉 new
之后,代码会变得更加干净。
class ActionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
将左边的抽出来作为变量
Widget left = Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
短多了啊*/'Title',
style: TextStyle(fontSize: 16),
),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
'Desc',
style: TextStyle(fontSize: 12),
),
),
],
),
);
右边同理
Widget right = Row(
children: <Widget>[
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
短多了啊*/child: Text('Edit'),
onPressed: () {
something here');
},
),
),
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: () {
something here');
},
),
),
],
);
return AnimatedOpacity(
opacity: 1,
duration: Duration(milliseconds: 800),
child: Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Expanded(
left, :
),
right,
现在有六个反括号
),
),
);
}
}
class ActionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
这里看起来非常清晰,我们就不需要继续抽离变量了
Widget left = Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Title',
style: TextStyle(fontSize: 16),
),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
'Desc',
style: TextStyle(fontSize: 12),
),
),
],
),
);
Widget right = Row(
children: <Widget>[
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
child: Text('Edit'),
onPressed: () {
something here');
},
),
),
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: () {
something here');
},
),
),
],
);
定义变量
Widget row = Row(
children: <Widget>[
Expanded(
child: left,
),
right,
],
);
然后在外面嵌套修饰的Container,注意,这里把row嵌套给了自己
row = Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: row,
);
我突然觉得这一层Widget暂时不需要,使用注释就可以将其去掉
如果这里是嵌套的写法,是不能快速注释一个Widget的
row = AnimatedOpacity(
opacity: 1,
duration: Duration(milliseconds: 800),
child: row,
);
return row;
}
}
现在看起来就好多啦
class ActionRow extends StatelessWidget {
final String title;
final String desc;
final VoidCallback onEdit;
final VoidCallback onDelete;
如上文所述,这里是自动生成的,然后添加一下默认值
const ActionRow({
Key key,
'title', :
'desc', :
this.onEdit,
this.onDelete,
super(key: key); :
@override
Widget build(BuildContext context) {
Widget left = Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontSize: 16),
),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
desc,
style: TextStyle(fontSize: 12),
),
),
],
),
);
Widget right = Container(
alignment: Alignment.center,
child: Text('No Function Here'),
);
只有传入方法,右边才会出现按钮
if (onEdit != null || onDelete != null) {
right = Row(
children: <Widget>[
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
child: Text('Edit'),
onPressed: onEdit ?? () {},
),
),
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: onDelete ?? () {},
),
),
],
);
}
Widget row = Row(
children: <Widget>[
Expanded(
child: left,
),
right,
],
);
row = Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: row,
);
return row;
}
}
本文适合想想: 为什么不是 Stateful 的 Widget?
这一步也有快捷操作哦:
抽离后的代码:
class ActionRow extends StatelessWidget {
final String title;
final String desc;
final VoidCallback onEdit;
final VoidCallback onDelete;
const ActionRow({
Key key,
this.title: 'title',
this.desc: 'desc',
this.onEdit,
this.onDelete,
}) : super(key: key);
Widget build(BuildContext context) {
// 这个就很少了
Widget left = TextGroup(title: title, desc: desc);
Widget right = Container(
alignment: Alignment.center,
child: Text('No Function Here'),
);
if (onEdit != null || onDelete != null) {
right = Row(
children: <Widget>[
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
child: Text('Edit'),
onPressed: onEdit ?? () {},
),
),
Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: onDelete ?? () {},
),
),
],
);
}
Widget row = Row(
children: <Widget>[
Expanded(
child: left,
),
right,
],
);
row = Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: row,
);
// row = AnimatedOpacity(
// opacity: 1,
// duration: Duration(milliseconds: 800),
// child: row,
// );
return row;
}
}
// 没必要优化抽离后的小Widget,毕竟只需要知道他负责显示两行字就好了
// 看上去代码很多,但是都是自动生成的
class TextGroup extends StatelessWidget {
const TextGroup({
Key key,
this.title,
this.desc,
}) : super(key: key);
final String title;
final String desc;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontSize: 16),
),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
desc,
style: TextStyle(fontSize: 12),
),
),
],
),
);
}
}
△ 优化前 △ 优化后
误区
Google
的部分 UI 源码也存在如下这些问题,导致阅读困难,但是有部分官方
Widget
的代码质量明显更好,我们当然可以学习更好的写法。
使用 function 来创建 Widget
不必使用 function
来创建 Widget
,你应当把组件提取成 StatelessWidget
,然后将属性或事件传递给这个 Widget
。
使用 function
的问题是,你可以在 function
中向 Widget 传递闭包,该闭包包含了当前的作用域,却又不在 build
方法中,同时你也可以在 function
中做其他无关的事情。
所以当我们过一段时间回头阅读代码的时候,build
中夹杂的 function
显得非常的混乱不堪,没有条理,UI 应当是聚合在一起的,而数据与事件,应当与 UI 分离开来。如此才可以阅读一次 build 方法,就基本理解当前 Widget 的功能与目的。
// function创建Widget可能会破坏Widget树的可读性
class ActionRow extends StatelessWidget {
final String title;
final String desc;
final VoidCallback onEdit;
final VoidCallback onDelete;
const ActionRow({
Key key,
this.title: 'title',
this.desc: 'desc',
this.onEdit,
this.onDelete,
}) : super(key: key);
Widget buildEditButton() {
return Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.orange,
child: Text('Edit'),
onPressed: onEdit ?? () {},
),
);
}
Widget buildDeleteButton() {
return Container(
padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
child: MaterialButton(
color: Colors.red,
child: Text('Delete'),
onPressed: onDelete ?? () {},
),
);
}
Widget build(BuildContext context) {
// Widget left = TextGroup(title: title, desc: desc);
Widget right = Container(
alignment: Alignment.center,
child: Text('No Function Here'),
);
if (onEdit != null || onDelete != null) {
// 本来这里要传入onDelete和onEdit的,
// 但是现在这两个属性根本就不在build方法里出现(他们去哪儿了?),
// 所以使用function来build组件可能会丢失一些关键信息,打断代码阅读的顺序。
Widget editButton = buildEditButton();
Widget deleteButton = buildDeleteButton();
right = Row(
children: <Widget>[
editButton,
deleteButton,
],
);
}
Widget row = Row(
children: <Widget>[
// Expanded(
// child: left,
// ),
right,
],
);
row = Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 1),
padding: EdgeInsets.symmetric(horizontal: 20),
child: row,
);
return row;
}
}
function
中切来切去,属性引用没有任何章法可言。
StatelessWidget
会强制所有属性都是
final
的,这意味着,你必须把可变的属性写在 build 方法里 (而不是其他地方),大多数时候,这非常有利于代码阅读。
因为 final 的特性,你也没机会把变量写到其他地方了,这样看起来更整洁,毕竟整个页面的数据通常也只有那么几个。
写太多 StatefulWidget
这里其实说的是,不要嵌套很多 StatefulWidget
,事实上大部分 Widget 都可以是 Stateless
的: 例如官方的 Switch
组件,居然也是 Stateless
的。通常按照我们的经验,Switch
似乎需要维护自己的开关状态,在 Flutter 实际应用中,并不需要如此,任何状态都可以交给父组件管理,从而减少一个 StatefulWidget
,也就减少了一个 State
,大大减少了 UI 代码的复杂程度。
Stateful
的:
页面,推荐每一个返回
Scaffold
的Widget
都写成Stateful
的。需要在
initState
中触发方法,例如从网络请求数据,开启蓝牙搜索等异步操作。需要维护自己的动画状态的。
同时 StatefulWidget
不应紧密嵌套在一起,只需要把数据都放在上一级的 state
里就好,维护 state
实际上会多出非常多的无用代码,过多嵌套会直接导致代码混乱不堪。
总结
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 了解更多 "开发者说·DTalk" 活动详情与参与方式
长按右侧二维码
报名参与