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

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

优化效果(缩略图):html

clipboard.png

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

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

初步介绍

咱们先来简单看一下,Flutter的UI代码:网络

使用build方法

FlutterWidget使用build方法来建立UI组件,而后经过注入child属性的方式为组件添加子组件,子组件能够继续包含child,经过调用每个childbuild方法,就造成了相似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属性,点击“快速修复”,就能够自动生成构造方法。

clipboard.png

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,可想而知它被嵌套在了多么深的位置。ide

clipboard.png

来看看这个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;
  }
}

提取组件——Stateful与Stateless

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

想一想:为何不是 StatefulWidget

这一步也有快捷操做哦:

clipboard.png

抽离后的代码:

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),
            ),
          ),
        ],
      ),
    );
  }
}

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

优化完成,看看缩略图:

优化前:
clipboard.png
优化后:
clipboard.png

误区

不少开发者会有以下误区。实际上,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的:

  1. 页面,推荐每个返回ScaffoldWidget都写成Stateful
  2. 须要在initState中触发方法,例如从网络请求数据,开启蓝牙搜索等异步操做。
  3. 须要维护本身的动画状态的。

同时StatefulWidget不该紧密嵌套在一块儿,只须要把数据都放在上一级的state里就好,维护state实际上会多出很是多的无用代码,过多嵌套会直接致使代码混乱不堪。

总结

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

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

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

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

相关文章
相关标签/搜索