资讯专栏INFORMATION COLUMN

【开发经验】Flutter避免代码嵌套,写好build方法

Worktile / 980人阅读

摘要:意味着属性必须在构造函数中就被初始化完成,不接受提前定义,也不接受更改。所以,在生命周期中动态的改变对象的属性是不可能的,必须使用框架的方法来为构造函数动态指定参数,从而达到改变组件属性的功能。

</>复制代码

  1. 本文适合使用Flutter开发过一段时间的开发者阅读,旨在分享一种避免Flutter的UI代码嵌套太深问题的方法。如果对本文内容或观点有相关疑问,欢迎在评论中指出。

优化效果(缩略图):

距离我接触Flutter已经过去了九个月,在Flutter代码编写的过程中,很多开发者都遇到了“回调地狱”的问题。在Flutter中,称之为回调并不准确,准确的说,是因为众多Widget互相嵌套在一起,导致反括号部分堆积严重,极度影响代码可读性。

本文将介绍一种代码编写风格,最大限度减少嵌套对代码阅读的影响。

初步介绍

我们先来简单看一下,Flutter的UI代码:

使用build方法

FlutterWidget使用build方法来创建UI组件,然后通过注入child属性的方式为组件添加子组件,子组件可以继续包含child,通过调用每一个childbuild方法,就形成了类似DOM结构的组件树,然后由渲染引擎渲染图形。

一个常见的定义组件的例子如下:

</>复制代码

  1. class DeleteText extends StatelessWidget {
  2. // 我们在build方法中渲染自定义Widget
  3. @override
  4. Widget build(BuildContext context) {
  5. return Text("Delete");
  6. }
  7. }
组件属性必须为final

要在Flutter中定义(继承)一个Widget,则它的属性必须都是final的。final意味着属性必须在构造函数中就被初始化完成,不接受提前定义,也不接受更改。所以,在生命周期中动态的改变Widget对象的属性是不可能的,必须使用框架的build方法来为构造函数动态指定参数,从而达到改变组件属性的功能。

</>复制代码

  1. class Avatar extends StatelessWidget {
  2. // 如果url属性不是final的,编译器会报出警告
  3. final String url;
  4. // 这个构造方法很长,但是主要你写了final属性,VSCode就会帮我们自动生成
  5. const Avatar({Key key, this.url}) : super(key: key);
  6. @override
  7. Widget build(BuildContext context) {
  8. return Container(
  9. decoration: BoxDecoration(
  10. borderRadius: BorderRadius.circular(8),
  11. ),
  12. child: Image.network(url),
  13. );
  14. }
  15. }

</>复制代码

  1. Tips:自动创建构造方法,只要是构造方法没有的final属性,点击“快速修复”,就可以自动生成构造方法。

Flutter语法与HTML/CSS

嵌套正是DOM树的特点,正如HTML其实也会无限嵌套一样(大多数前端可能看HTML看习惯了,都忘了HTML其实也经常会写成嵌套很深的形式),Flutter的UI代码嵌套本质是不可避免的,这正是Flutter UI代码的编写特点——一次成型,而不是通过addView之类的方法来手动管理每一个视图的生命周期。在此基础上,Flutter可以高效的反复重建Widget,在渲染效率上展现出了非常大的优势。

</>复制代码

嵌套代码难以阅读

当我们评判一串代码的时候,一个显而易见的点,就是代码距离左边的距离,如果一行代码距离左边达到了十多个tab,可想而知它被嵌套在了多么深的位置。

来看看这个Widget,这个Widget很简单,左边有一个正文和一个附属文本,附属文本在正文下方,右边有一组按钮,代表这一行的操作,我们再给他嵌套一个动画的渐现效果,处理好字体。那么他的代码应该如下所示:

</>复制代码

  1. // 一个简单的嵌套的情况
  2. class ActionRow extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. return AnimatedOpacity(
  6. opacity: 1,
  7. duration: Duration(milliseconds: 800),
  8. child: Container(
  9. color: Colors.white,
  10. margin: EdgeInsets.symmetric(vertical: 1),
  11. padding: EdgeInsets.symmetric(horizontal: 20),
  12. child: Row(
  13. children: [
  14. Expanded(
  15. child: Container(
  16. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  17. child: Column(
  18. crossAxisAlignment: CrossAxisAlignment.start,
  19. children: [
  20. /* 超级长的左边距 */Text(
  21. "Title",
  22. style: TextStyle(fontSize: 16),
  23. ),
  24. Container(
  25. padding: EdgeInsets.only(top: 4),
  26. child: Text(
  27. "Desc",
  28. style: TextStyle(fontSize: 12),
  29. ),
  30. ),
  31. ],
  32. ),
  33. ),
  34. ),
  35. Row(
  36. children: [
  37. Container(
  38. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  39. child: MaterialButton(
  40. color: Colors.orange,
  41. child: Text("Edit"),
  42. /* 超级长的左边距 */onPressed: () {
  43. print("Handle Edit");
  44. },
  45. ),
  46. ),
  47. Container(
  48. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  49. child: MaterialButton(
  50. color: Colors.red,
  51. child: Text("Delete"),
  52. onPressed: () {
  53. print("Handle Delete");
  54. },// 往下数,足足11个反括号
  55. ),
  56. ),
  57. ],
  58. )
  59. ],
  60. ),
  61. ),
  62. );
  63. }
  64. }

此种代码,只要是开发过Flutter的开发者一定不会陌生,它可以完美运行,但是十分难以阅读。反括号的数量经常会达到一个更夸张的级别,导致部分内容被顶到过于右边,在阅读时造成了非常大的困难。

就让我们以这串代码为例子,来优化他的嵌套,使其可以轻松的从上到下阅读。

解决方法 不写new

Dart2已经可以完全不写new了,但有的开发者还在写new。去掉new之后,代码会变得更加干净。

定义变量以减少反括号

在这里,我们可以抽取部分嵌套很深的Widget,将其定义成变量,从而减少它与左边的距离。
读一下代码,我们很容易就能发现,左边的Expanded部分中,两个文字的相关代码距离左边太远了,我们将他们抽出来作为一个独立的Widget变量,右边的两个按钮也是同理:

</>复制代码

  1. class ActionRow extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. // 将左边的抽出来作为变量
  5. Widget left = Container(
  6. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  7. child: Column(
  8. crossAxisAlignment: CrossAxisAlignment.start,
  9. children: [
  10. Text(
  11. /* 短多了啊*/"Title",
  12. style: TextStyle(fontSize: 16),
  13. ),
  14. Container(
  15. padding: EdgeInsets.only(top: 4),
  16. child: Text(
  17. "Desc",
  18. style: TextStyle(fontSize: 12),
  19. ),
  20. ),
  21. ],
  22. ),
  23. );
  24. // 右边同理
  25. Widget right = Row(
  26. children: [
  27. Container(
  28. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  29. child: MaterialButton(
  30. color: Colors.orange,
  31. /* 短多了啊*/child: Text("Edit"),
  32. onPressed: () {
  33. print("Do something here");
  34. },
  35. ),
  36. ),
  37. Container(
  38. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  39. child: MaterialButton(
  40. color: Colors.red,
  41. child: Text("Delete"),
  42. onPressed: () {
  43. print("Do something here");
  44. },
  45. ),
  46. ),
  47. ],
  48. );
  49. return AnimatedOpacity(
  50. opacity: 1,
  51. duration: Duration(milliseconds: 800),
  52. child: Container(
  53. color: Colors.white,
  54. margin: EdgeInsets.symmetric(vertical: 1),
  55. padding: EdgeInsets.symmetric(horizontal: 20),
  56. child: Row(
  57. children: [
  58. Expanded(
  59. /*这里还是太长*/child: left,
  60. ),
  61. right,
  62. ],// 现在有六个反括号
  63. ),
  64. ),
  65. );
  66. }
  67. }

现在,我们的程序似乎有了一个均匀的左边距,看起来不会那么可怕了。

反复利用变量,处理复杂嵌套

在嵌套很复杂时,也可以使用这种处理方法,把修饰用的UI与主体功能分离。很多时候为了实现设计图我们会嵌套很多的Center和Padding,将他们与真正起作用的UI分离开,有利于我们第一时间找到目标Widget:

</>复制代码

  1. class ActionRow extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. // 这里看起来非常清晰,我们就不需要继续抽离变量了
  5. Widget left = Container(
  6. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  7. child: Column(
  8. crossAxisAlignment: CrossAxisAlignment.start,
  9. children: [
  10. Text(
  11. "Title",
  12. style: TextStyle(fontSize: 16),
  13. ),
  14. Container(
  15. padding: EdgeInsets.only(top: 4),
  16. child: Text(
  17. "Desc",
  18. style: TextStyle(fontSize: 12),
  19. ),
  20. ),
  21. ],
  22. ),
  23. );
  24. Widget right = Row(
  25. children: [
  26. Container(
  27. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  28. child: MaterialButton(
  29. color: Colors.orange,
  30. child: Text("Edit"),
  31. onPressed: () {
  32. print("Do something here");
  33. },
  34. ),
  35. ),
  36. Container(
  37. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  38. child: MaterialButton(
  39. color: Colors.red,
  40. child: Text("Delete"),
  41. onPressed: () {
  42. print("Do something here");
  43. },
  44. ),
  45. ),
  46. ],
  47. );
  48. // 定义变量
  49. Widget row = Row(
  50. children: [
  51. Expanded(
  52. child: left,
  53. ),
  54. right,
  55. ],
  56. );
  57. // 然后在外面嵌套修饰的Container,注意,这里把row嵌套给了自己
  58. row = Container(
  59. color: Colors.white,
  60. margin: EdgeInsets.symmetric(vertical: 1),
  61. padding: EdgeInsets.symmetric(horizontal: 20),
  62. child: row,
  63. );
  64. // 我突然觉得这一层Widget暂时不需要,使用注释就可以将其去掉
  65. // 如果这里是嵌套的写法,是不能快速注释一个Widget的
  66. // row = AnimatedOpacity(
  67. // opacity: 1,
  68. // duration: Duration(milliseconds: 800),
  69. // child: row,
  70. // );
  71. return row;
  72. }
  73. }
反复利用变量完成条件渲染

有时候,在数据不同时,我们希望组件按不同的方式嵌套。将组件写成一整坨当然做不到如此灵活,从google的AppBar的源码中,我学习了一套写法,通过反复利用同一个Widget,优雅的处理了条件渲染的问题。

在这个例子里,我们希望做到一个效果,如果没有传入onEdit与onDelete方法,就不渲染右边的部分,应该如何写呢?这个时候,嵌套任何组件都显得复杂,我们只需要一个if就搞定了。

</>复制代码

  1. // 现在看起来就好多啦
  2. class ActionRow extends StatelessWidget {
  3. final String title;
  4. final String desc;
  5. final VoidCallback onEdit;
  6. final VoidCallback onDelete;
  7. // 如上文所述,这里是自动生成的,然后添加一下默认值
  8. const ActionRow({
  9. Key key,
  10. this.title: "title",
  11. this.desc: "desc",
  12. this.onEdit,
  13. this.onDelete,
  14. }) : super(key: key);
  15. @override
  16. Widget build(BuildContext context) {
  17. Widget left = Container(
  18. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  19. child: Column(
  20. crossAxisAlignment: CrossAxisAlignment.start,
  21. children: [
  22. Text(
  23. title,
  24. style: TextStyle(fontSize: 16),
  25. ),
  26. Container(
  27. padding: EdgeInsets.only(top: 4),
  28. child: Text(
  29. desc,
  30. style: TextStyle(fontSize: 12),
  31. ),
  32. ),
  33. ],
  34. ),
  35. );
  36. Widget right = Container(
  37. alignment: Alignment.center,
  38. child: Text("No Function Here"),
  39. );
  40. // 只有传入方法,右边才会出现按钮
  41. if (onEdit != null || onDelete != null) {
  42. right = Row(
  43. children: [
  44. Container(
  45. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  46. child: MaterialButton(
  47. color: Colors.orange,
  48. child: Text("Edit"),
  49. onPressed: onEdit ?? () {},
  50. ),
  51. ),
  52. Container(
  53. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  54. child: MaterialButton(
  55. color: Colors.red,
  56. child: Text("Delete"),
  57. onPressed: onDelete ?? () {},
  58. ),
  59. ),
  60. ],
  61. );
  62. }
  63. Widget row = Row(
  64. children: [
  65. Expanded(
  66. child: left,
  67. ),
  68. right,
  69. ],
  70. );
  71. row = Container(
  72. color: Colors.white,
  73. margin: EdgeInsets.symmetric(vertical: 1),
  74. padding: EdgeInsets.symmetric(horizontal: 20),
  75. child: row,
  76. );
  77. return row;
  78. }
  79. }
提取组件——Stateful与Stateless

很显然上面的代码属于比较简单的UI代码,我们通常会把代码写的更大更复杂,这时候抽取组件就十分有必要,在上面的代码中,我们觉得left还是有点复杂的,试着把它抽出来,作为一个StatelessWidget:

</>复制代码

  1. 想想:为什么不是StatefulWidget

这一步也有快捷操作哦:

抽离后的代码:

</>复制代码

  1. class ActionRow extends StatelessWidget {
  2. final String title;
  3. final String desc;
  4. final VoidCallback onEdit;
  5. final VoidCallback onDelete;
  6. const ActionRow({
  7. Key key,
  8. this.title: "title",
  9. this.desc: "desc",
  10. this.onEdit,
  11. this.onDelete,
  12. }) : super(key: key);
  13. @override
  14. Widget build(BuildContext context) {
  15. // 这个就很少了
  16. Widget left = TextGroup(title: title, desc: desc);
  17. Widget right = Container(
  18. alignment: Alignment.center,
  19. child: Text("No Function Here"),
  20. );
  21. if (onEdit != null || onDelete != null) {
  22. right = Row(
  23. children: [
  24. Container(
  25. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  26. child: MaterialButton(
  27. color: Colors.orange,
  28. child: Text("Edit"),
  29. onPressed: onEdit ?? () {},
  30. ),
  31. ),
  32. Container(
  33. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  34. child: MaterialButton(
  35. color: Colors.red,
  36. child: Text("Delete"),
  37. onPressed: onDelete ?? () {},
  38. ),
  39. ),
  40. ],
  41. );
  42. }
  43. Widget row = Row(
  44. children: [
  45. Expanded(
  46. child: left,
  47. ),
  48. right,
  49. ],
  50. );
  51. row = Container(
  52. color: Colors.white,
  53. margin: EdgeInsets.symmetric(vertical: 1),
  54. padding: EdgeInsets.symmetric(horizontal: 20),
  55. child: row,
  56. );
  57. // row = AnimatedOpacity(
  58. // opacity: 1,
  59. // duration: Duration(milliseconds: 800),
  60. // child: row,
  61. // );
  62. return row;
  63. }
  64. }
  65. // 没必要优化抽离后的小Widget,毕竟只需要知道他负责显示两行字就好了
  66. // 看上去代码很多,但是都是自动生成的
  67. class TextGroup extends StatelessWidget {
  68. const TextGroup({
  69. Key key,
  70. @required this.title,
  71. @required this.desc,
  72. }) : super(key: key);
  73. final String title;
  74. final String desc;
  75. @override
  76. Widget build(BuildContext context) {
  77. return Container(
  78. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  79. child: Column(
  80. crossAxisAlignment: CrossAxisAlignment.start,
  81. children: [
  82. Text(
  83. title,
  84. style: TextStyle(fontSize: 16),
  85. ),
  86. Container(
  87. padding: EdgeInsets.only(top: 4),
  88. child: Text(
  89. desc,
  90. style: TextStyle(fontSize: 12),
  91. ),
  92. ),
  93. ],
  94. ),
  95. );
  96. }
  97. }

如此一来我们的优化就完成了,对比一下代码,是不是看起来更好了呢?

优化完成,看看缩略图:

优化前:

优化后:

误区

很多开发者会有如下误区。实际上,Google的部分UI源码也存在如下这些问题,导致阅读困难,但是有部分官方Widget的代码质量明显更好,我们当然可以学习更好的写法。

在编写UI代码时,请避免如下行为:

使用function来创建Widget

不必使用function来创建Widget,你应当把组件提取成StatelessWidget,然后将属性或事件传递给这个Widget

使用function的问题是,你可以在function中向Widget传递闭包,该闭包包含了当前的作用域,却又不在build方法中,同时你也可以在function中做其他无关的事情。

所以当我们过一段时间回头阅读代码的时候,build中夹杂的function显得非常的混乱不堪,没有条理,UI应当是聚合在一起的,而数据与事件,应当与UI分离开来。如此才可以阅读一次build方法,就基本理解当前Widget的功能与目的。

</>复制代码

  1. // function创建Widget可能会破坏Widget树的可读性
  2. class ActionRow extends StatelessWidget {
  3. final String title;
  4. final String desc;
  5. final VoidCallback onEdit;
  6. final VoidCallback onDelete;
  7. const ActionRow({
  8. Key key,
  9. this.title: "title",
  10. this.desc: "desc",
  11. this.onEdit,
  12. this.onDelete,
  13. }) : super(key: key);
  14. Widget buildEditButton() {
  15. return Container(
  16. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  17. child: MaterialButton(
  18. color: Colors.orange,
  19. child: Text("Edit"),
  20. onPressed: onEdit ?? () {},
  21. ),
  22. );
  23. }
  24. Widget buildDeleteButton() {
  25. return Container(
  26. padding: EdgeInsets.fromLTRB(6, 8, 8, 8),
  27. child: MaterialButton(
  28. color: Colors.red,
  29. child: Text("Delete"),
  30. onPressed: onDelete ?? () {},
  31. ),
  32. );
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. // Widget left = TextGroup(title: title, desc: desc);
  37. Widget right = Container(
  38. alignment: Alignment.center,
  39. child: Text("No Function Here"),
  40. );
  41. if (onEdit != null || onDelete != null) {
  42. // 本来这里要传入onDelete和onEdit的,
  43. // 但是现在这两个属性根本就不在build方法里出现(他们去哪儿了?),
  44. // 所以使用function来build组件可能会丢失一些关键信息,打断代码阅读的顺序。
  45. Widget editButton = buildEditButton();
  46. Widget deleteButton = buildDeleteButton();
  47. right = Row(
  48. children: [
  49. editButton,
  50. deleteButton,
  51. ],
  52. );
  53. }
  54. Widget row = Row(
  55. children: [
  56. // Expanded(
  57. // child: left,
  58. // ),
  59. right,
  60. ],
  61. );
  62. row = Container(
  63. color: Colors.white,
  64. margin: EdgeInsets.symmetric(vertical: 1),
  65. padding: EdgeInsets.symmetric(horizontal: 20),
  66. child: row,
  67. );
  68. return row;
  69. }
  70. }

这个当然不是强制的,甚至不少Google的例子也采用这种写法,但是通过阅读大量的源码来进行对比,这种写法是很难通顺阅读的,总是需要在不同的function中切来切去,属性引用没有任何章法可言。

StatelessWidget会强制所有属性都是final的,这意味着,你必须把可变的属性写在build方法里(而不是其他地方),大多数时候,这非常有利于代码阅读。

</>复制代码

  1. 因为final的特性,你也没机会把变量写到其他地方了,这样看起来更整洁,毕竟整个页面的数据通常也只有那么几个。
写太多StatefulWidget

这里其实说的是,不要嵌套很多StatefulWidget,事实上大部分Widget都可以是Stateless的:例如官方的Switch组件,居然也是Stateless的。通常按照我们的经验,Switch似乎需要维护自己的开关状态,在Flutter实际应用中,并不需要如此,任何状态都可以交给父组件管理,从而减少一个StatefulWidget,也就减少了一个State,大大减少了UI代码的复杂程度。

从我目前的经验来看,只有很少部分Widget需要写成Stateful的:

页面,推荐每一个返回ScaffoldWidget都写成Stateful

需要在initState中触发方法,例如从网络请求数据,开启蓝牙搜索等异步操作。

需要维护自己的动画状态的。

同时StatefulWidget不应紧密嵌套在一起,只需要把数据都放在上一级的state里就好,维护state实际上会多出非常多的无用代码,过多嵌套会直接导致代码混乱不堪。

总结

作者:马嘉伦
日期:2019/07/14
平台:Segmentfault独家,勿转载

我的其他文章:
【开发经验】浅谈flutter的优点与缺点
【Flutter工具】fmaker:自动生成倍率切图/自动更换App图标
【开发经验】在Flutter中使用dart的单例模式

本文是对Flutter的一种编码风格的概括,主要的意义在于减少代码嵌套层数,增强代码可读性。本文大部分经验其实来自Google自己的组件源码,是通过对比大量源码得出的一个较优写法,如果你对上述观点,建议,代码,风格有疑问或者发现了文章中的问题,请直接留下你的评论,我会直接在评论中进行回复。

本文禁止任何转载,需转载授权可直接联系我

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105633.html

相关文章

  • flutter笔记3:基础语法、框架、控件

    摘要:是啥是谷歌推出的一套视觉设计语言。比如有的可以换皮肤,而每一套皮肤就是一种设计语言,有古典风呀炫酷风呀极简风呀神马的,而就是谷歌风,有兴趣的同学可以学习了解一下官方原版和中文翻译版,这是每一个产品经理的必修教材。 flutter环境和运行环境搭建好之后,可以开始撸码了,然而当你打开VScode,在打开项目文件夹后,摆在你面前的是main.dart被打开的样子,里面七七八八的已经写好了一...

    draveness 评论0 收藏0
  • flutter笔记4:使用material原生控件开发一个APP

    摘要:体验热更新带来的开发周期加速。学会使用有状态控件,增强了应用的交互。使用和创建了一个支持懒加载的无限滚动列表。了解如何使用主题更改应用的外观。 接着上一篇,我们做一个这样的APP:showImg(https://segmentfault.com/img/remote/1460000013672700); 开始之前,我发现了一个好玩的东西,每次我们在终端中输入命令: flutter ru...

    lifefriend_007 评论0 收藏0
  • flutter的入门实践到可开发

    摘要:继上一篇关于的介绍,是仿照微信界面,因为作为前端开发,有一定的基础,所有写起来,也不是很吃力。班门弄斧之作,若有大神见到,敬请指教,有不对不合理之处,敬请指出我是迩伶贰环境准备以系统为例。 flutter的入门记录 0.前言: flutter 的入门demo 已经写好一个星期了,只不过一直都没有整理出博客来。收拾好心情,来整理一下。继上一篇关于react-native-wx的介绍,是仿...

    _DangJin 评论0 收藏0
  • 用前端 最舒服的躺姿 "搞定" Flutter (组件篇)

    摘要:是谷歌的移动框架,可以快速在和上构建高质量的原生用户界面。在全世界好了这些,大家早就知道了,来点实在的话说隔壁师兄,闲鱼是最早一批与谷歌展开合作,并在重要的商品详情页中使用技术上线的。一切皆来自的组件皆来自。是状态不可变的称为无状态。 前言 要说2018年最火的跨端技术,当属于 Flutter 莫属,应该没人质疑吧。一个新的技术的趋势,最明显的特征,就是它一定想把前浪拍死在沙滩上。这个...

    LMou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<