本文适合使用Flutter开发过一段时间的开发者阅读,旨在分享一种避免
Flutter
的UI代码嵌套太深问题的方法。若是对本文内容或观点有相关疑问,欢迎在评论中指出。
优化效果(缩略图):html
距离我接触Flutter已通过去了九个月,在Flutter代码编写的过程当中,不少开发者都遇到了“回调地狱”的问题。在Flutter
中,称之为回调并不许确,准确的说,是由于众多Widget
互相嵌套在一块儿,致使反括号部分堆积严重,极度影响代码可读性。前端
本文将介绍一种代码编写风格,最大限度减小嵌套对代码阅读的影响。segmentfault
咱们先来简单看一下,Flutter
的UI代码:网络
build
方法Flutter
的Widget
使用build
方法来建立UI组件,而后经过注入child
属性的方式为组件添加子组件,子组件能够继续包含child
,经过调用每个child
的build
方法,就造成了相似DOM结构的组件树,而后由渲染引擎渲染图形。 闭包
一个常见的定义组件的例子以下:框架
class DeleteText extends StatelessWidget { // 咱们在build方法中渲染自定义Widget @override Widget build(BuildContext context) { return Text('Delete'); } }
final
要在Flutter
中定义(继承)一个Widget,则它的属性必须都是final
的。final
意味着属性必须在构造函数中就被初始化完成,不接受提早定义,也不接受更改。因此,在生命周期中动态的改变Widget
对象的属性是不可能的,必须使用框架的build
方法来为构造函数动态指定参数,从而达到改变组件属性的功能。less
class Avatar extends StatelessWidget { // 若是url属性不是final的,编译器会报出警告 final String url; // 这个构造方法很长,可是主要你写了final属性,VSCode就会帮咱们自动生成 const Avatar({Key key, this.url}) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), child: Image.network(url), ); } }
Tips:自动建立构造方法,只要是构造方法没有的final属性,点击“快速修复”,就能够自动生成构造方法。
嵌套正是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,可想而知它被嵌套在了多么深的位置。ide
来看看这个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: () { print('Handle Edit'); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text('Delete'), onPressed: () { print('Handle Delete'); },// 往下数,足足11个反括号 ), ), ], ) ], ), ), ); } }
此种代码,只要是开发过Flutter
的开发者必定不会陌生,它能够完美运行,可是十分难以阅读。反括号的数量常常会达到一个更夸张的级别,致使部份内容被顶到过于右边,在阅读时形成了很是大的困难。
就让咱们以这串代码为例子,来优化他的嵌套,使其能够轻松的从上到下阅读。
new
Dart2
已经能够彻底不写new
了,但有的开发者还在写new
。去掉new
以后,代码会变得更加干净。
在这里,咱们能够抽取部分嵌套很深的Widget,将其定义成变量,从而减小它与左边的距离。
读一下代码,咱们很容易就能发现,左边的Expanded部分中,两个文字的相关代码距离左边太远了,咱们将他们抽出来做为一个独立的Widget变量,右边的两个按钮也是同理:
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: () { print('Do something here'); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text('Delete'), onPressed: () { print('Do 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( /*这里仍是太长*/child: left, ), right, ],// 如今有六个反括号 ), ), ); } }
如今,咱们的程序彷佛有了一个均匀的左边距,看起来不会那么可怕了。
在嵌套很复杂时,也可使用这种处理方法,把修饰用的UI与主体功能分离。不少时候为了实现设计图咱们会嵌套不少的Center和Padding,将他们与真正起做用的UI分离开,有利于咱们第一时间找到目标Widget:
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: () { print('Do something here'); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text('Delete'), onPressed: () { print('Do 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; } }
有时候,在数据不一样时,咱们但愿组件按不一样的方式嵌套。将组件写成一整坨固然作不到如此灵活,从google的AppBar的源码中,我学习了一套写法,经过反复利用同一个Widget,优雅的处理了条件渲染的问题。
在这个例子里,咱们但愿作到一个效果,若是没有传入onEdit与onDelete方法,就不渲染右边的部分,应该如何写呢?这个时候,嵌套任何组件都显得复杂,咱们只须要一个if就搞定了。
// 如今看起来就好多啦 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); @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; } }
很显然上面的代码属于比较简单的UI代码,咱们一般会把代码写的更大更复杂,这时候抽取组件就十分有必要,在上面的代码中,咱们以为left仍是有点复杂的,试着把它抽出来,做为一个StatelessWidget:
想一想:为何不是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); @override 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, @required this.title, @required this.desc, }) : super(key: key); final String title; final String desc; @override 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
的代码质量明显更好,咱们固然能够学习更好的写法。
在编写UI代码时,请避免以下行为:
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 ?? () {}, ), ); } @override 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; } }
这个固然不是强制的,甚至很多Google的例子也采用这种写法,可是经过阅读大量的源码来进行对比,这种写法是很难通顺阅读的,老是须要在不一样的function
中切来切去,属性引用没有任何章法可言。
而StatelessWidget
会强制全部属性都是final
的,这意味着,你必须把可变的属性写在build方法里(而不是其余地方),大多数时候,这很是有利于代码阅读。
由于
final
的特性,你也没机会把变量写到其余地方了,这样看起来更整洁,毕竟整个页面的数据一般也只有那么几个。
StatefulWidget
这里其实说的是,不要嵌套不少StatefulWidget
,事实上大部分Widget均可以是Stateless
的:例如官方的Switch
组件,竟然也是Stateless
的。一般按照咱们的经验,Switch
彷佛须要维护本身的开关状态,在Flutter实际应用中,并不须要如此,任何状态均可以交给父组件管理,从而减小一个StatefulWidget
,也就减小了一个State
,大大减小了UI代码的复杂程度。
从我目前的经验来看,只有不多部分Widget须要写成Stateful
的:
Scaffold
的Widget
都写成Stateful
的initState
中触发方法,例如从网络请求数据,开启蓝牙搜索等异步操做。同时StatefulWidget
不该紧密嵌套在一块儿,只须要把数据都放在上一级的state
里就好,维护state
实际上会多出很是多的无用代码,过多嵌套会直接致使代码混乱不堪。
做者:马嘉伦
日期:2019/07/14
平台:Segmentfault独家,勿转载
个人其余文章:
【开发经验】浅谈flutter的优势与缺点
【Flutter工具】fmaker:自动生成倍率切图/自动更换App图标
【开发经验】在Flutter中使用dart的单例模式
本文是对Flutter的一种编码风格的归纳,主要的意义在于减小代码嵌套层数,加强代码可读性。本文大部分经验其实来自Google
本身的组件源码,是经过对比大量源码得出的一个较优写法,若是你对上述观点,建议,代码,风格有疑问或者发现了文章中的问题,请直接留下你的评论,我会直接在评论中进行回复。