重构如何改善Flutter应用程序的可读性、可维护性以及提升性能

原文地址:How refactoring improve readability, maintainability and performance optimization of your Flutter application
原作者:Jonathan Monga
读后感:
这篇文章是关于如何组织代码结构的,如何编写Flutter代码,才能使代码有更好的可读性、可维护性,并且带来更好的性能呐,之前也翻译过一篇相似的文章Flutter Widget瘦身,两篇文章看完,想必会给你带一些收益。

前言

我们都同意widget 树是你在UI中所获得的东西,并且同意它完全是关于Flutter widget的,因此你可以将你的widget相互嵌套。无论你的UI是简单还是复杂,当你的UI简单时,即使几周后回来阅读你的代码,它也很容易阅读,并且性能很好,因为它展示的内容很少。但是当你的应用界面比较复杂时,这会促使你嵌套大量的widget,代码的可读性、可维护性降低,程序的效率也会降低。

我知道,对于初学者来说,很容易没有重构代码的文化,一旦注意力转移到其他事情上,初学者就会满足于widget的嵌套、嵌套、嵌套,这就是产生很深的widget树的原因。对于像我这样的新手Flutter开发者来说,这是很常见的现象,好吧,既然问题已经暴露出来了,我们怎么避免?如何以一种不陷入非常深的widget树的方式进行编码呐?

在我之前已经有不少人探讨过这个问题了,但我认为还是值得在花点时间再谈论一下。这个经常困扰我们的问题的答案就是代码重构。既然你已经得到了答案,那么就不要再拖延重构你的代码啦。下面我将用不同的技术,向你展示如何进行代码重构。

在向你展示如何重构代码之前,让我们使用此UI的代码:


Weather Stats.png

这个很漂亮的UI来自于https://github.com/JideGuru/weather_neumorphism_ui,这里并没有恶意,我不认为我比Olusegun Festus Babajide更厉害,以至于我有权利对他的代码做点评。同样你如果找到一些我的代码,我相信,你也会发现很多值得抱怨的地方。

不,这不是下流或者傲慢的行为,我将要做的无非只是专业的评论,这是我们都应该乐于做的事情,当完成时,我们应该欢迎它。通过这样发表评论,可以促使我们学习,医生这样做、飞行员这样做、律师这样做,我们程序员也应该学习这样做。补充一点:Olusegun Festus Babajide不仅是一位很好的Flutter开发人员,并且有勇气和善意,愿意将他的代码免费提供给整个社区,他把它提供给所有人看,并邀请公众使用和监督,这样做很赞。

这是现在的代码:


class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              height: 40,
              width: 40,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(10),
                color: Theme.of(context).primaryColor,
                boxShadow: [
                  BoxShadow(
                    offset: Offset(3, 3),
                    color: Colors.black12,
                    blurRadius: 5,
                  ),
                  BoxShadow(
                    offset: Offset(-3, -3),
                    color: Colors.white,
                    blurRadius: 5,
                  )
                ],
              ),
              child: Icon(
                Icons.arrow_back_ios,
                size: 14,
              ),
            ),
          ],
        ),
        centerTitle: true,
        elevation: 0,
        title: Text(
          "${Constants.appName}",
          style: TextStyle(
            fontSize: 25,
            fontWeight: FontWeight.w900,
          ),
        ),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(horizontal: 20),
        children: <Widget>[
          Container(
            height: 100,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Container(
                  height: 70,
                  width: MediaQuery.of(context).size.width,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(10),
                    color: Theme.of(context).primaryColor,
                    boxShadow: [
                      BoxShadow(
                        offset: Offset(3, 3),
                        color: Colors.black12,
                        blurRadius: 5,
                      ),
                      BoxShadow(
                        offset: Offset(-3, -3),
                        color: Colors.white,
                        blurRadius: 5,
                      )
                    ],
                  ),
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 20),
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        Row(
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: <Widget>[
                            Text(
                              "Period",
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                                color: Theme.of(context).textTheme.caption.color,
                              ),
                            ),

                            SizedBox(width: 30,),
                            Text(
                              "Last 30 days",
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                                fontSize: 16,
                              ),
                            ),
                          ],
                        ),

                        Container(
                          height: 40,
                          width: 40,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(10),
                            color: Theme.of(context).primaryColor,
                            boxShadow: [
                              BoxShadow(
                                offset: Offset(3, 3),
                                color: Colors.black12,
                                blurRadius: 5,
                              ),
                              BoxShadow(
                                offset: Offset(-3, -3),
                                color: Colors.white,
                                blurRadius: 5,
                              )
                            ],
                          ),
                          child: Icon(
                            Icons.arrow_forward_ios,
                            size: 14,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),

          SizedBox(height: 20,),

          Container(
            height: 300,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Container(
                  height: 280,
                  width: MediaQuery.of(context).size.width,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Theme.of(context).primaryColor,
                    boxShadow: [
                      BoxShadow(
                        offset: Offset(6, 6),
                        color: Colors.black12,
                        blurRadius: 5,
                      ),
                      BoxShadow(
                        offset: Offset(-6, -6),
                        color: Colors.white,
                        blurRadius: 5,
                      )
                    ],
                  ),
                  child: Stack(
                    children: <Widget>[
                      Align(
                        alignment: Alignment.center,
                        child: Icon(
                          Feather.loader,
                          size: 250,
                          color: Theme.of(context).accentColor,
                        ),
                      ),
                      Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: <Widget>[
                          Container(
                            height: 200,
                            width: MediaQuery.of(context).size.width,
                            decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              color: Theme.of(context).primaryColor,
                              boxShadow: [
                                BoxShadow(
                                  offset: Offset(3, 3),
                                  color: Colors.black12,
                                  blurRadius: 5,
                                ),
                                BoxShadow(
                                  offset: Offset(-3, -3),
                                  color: Colors.white,
                                  blurRadius: 5,
                                )
                              ],
                            ),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.center,
                              children: <Widget>[
                                Icon(
                                  Feather.thermometer,
                                  color: Theme.of(context).accentColor,
                                  size: 40,
                                ),
                                SizedBox(height: 20,),
                                Text(
                                  "7°C",
                                  style: TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 22,
                                    color: Theme.of(context).accentColor,
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),

          SizedBox(height: 20,),

          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Container(
                height: 150,
                width: 130,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Theme.of(context).primaryColor,
                  boxShadow: [
                    BoxShadow(
                      offset: Offset(3, 3),
                      color: Colors.black12,
                      blurRadius: 5,
                    ),
                    BoxShadow(
                      offset: Offset(-3, -3),
                      color: Colors.white,
                      blurRadius: 5,
                    ),
                  ],
                ),
                child: Padding(
                  padding: EdgeInsets.all(15),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      Icon(
                        Feather.cloud_snow,
                        size: 40,
                        color: Theme.of(context).accentColor,
                      ),

                      Text(
                        "Cool",
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 22,
                        ),
                      ),
                    ],
                  ),
                ),
              ),

              Neumorphic(
                height: 150,
                width: 130,
                status: NeumorphicStatus.convex,
                decoration: NeumorphicDecoration(
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Padding(
                  padding: EdgeInsets.all(15),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      Icon(
                        Feather.sun,
                        size: 40,
                        color: Colors.deepOrange,
                      ),

                      Text(
                        "Warm",
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 22,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          SizedBox(height: 20,),

          Neumorphic(
            status: NeumorphicStatus.convex,
            height: 50,
            decoration: NeumorphicDecoration(
              borderRadius: BorderRadius.circular(10),
            ),
            child: Center(
              child: Text(
                "Update Settings",
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 16,
                  color: Theme.of(context).accentColor,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
wow.png

那么让我们看看如何使这一切井然有序。

1、使用方法重构

我想你在某些地方已经看到了这种技术而没有意识到。该技术只是将widget作为方法调用的返回值,进行封装使用。假设在Flutter中一切都是widget,那么任何参与组成UI的类都继承自Widget类,该方法的返回值可能是任何一个widget类或者一些特定的类,例如容器类container、row、column等。

继续往下看,方法中的Widget可以依赖父widget的BuildContext实例或对象。这就是问题的来源,记住BuildContext对象知道widget在widget tree中的位置。既然此方法依赖于主BuildContext,那么当父widget重绘时,此方法也将强制重新组装、重新创建或者重绘其内部的widget。或者如果该方法也调用了其他依赖于父widget的BuildContext的方法,也会带来副作用,所有方法绘制他们的widget的次数将会和绘制父widget的次数一样多。无论哪种情况,这都不是我们重构后所期望的行为。

使用这种方法,我们将widget分割开来,这当然能够带来可读性及可维护性的提升,但是对于性能优化,并没有什么用处。当widget数量增加时,我们UI的性能在配置更改期间将会下降,例如屏幕旋转。

下面是两个方法的示例:

Column _buildLeadingColumn(BuildContext context) {
    return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Container(
            height: 40,
            width: 40,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10),
              color: Theme.of(context).primaryColor,
              boxShadow: [
                BoxShadow(
                  offset: Offset(3, 3),
                  color: Colors.black12,
                  blurRadius: 5,
                ),
                BoxShadow(
                  offset: Offset(-3, -3),
                  color: Colors.white,
                  blurRadius: 5,
                )
              ],
            ),
            child: Icon(
              Icons.arrow_back_ios,
              size: 14,
            ),
          ),
        ],
      );
  }
Widget _buildRow(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text(
          "Period",
          style: TextStyle(
            fontWeight: FontWeight.bold,
            color: Theme.of(context).textTheme.caption.color,
          ),
        ),
        SizedBox(
          width: 30,
        ),
        Text(
          "Last 30 days",
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 16,
          ),
        ),
      ],
    );
  }

我们使用Visual Studio Code作为代码编辑器(AS也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选中Refactor >Extract Method
4.在提取方法的弹窗中,输入_buildRow作为方法名,注意方法前的下划线,让Dart知道这是一个私有方法。
5.Row widget现在替换为了_b方法uildRow(),滚动到代码底部,方法和widget都得到了很好的重构。
6.继续重构其他的Rows、Columns、Containers和Stack Widget。

这种方式增加了代码的可读性,widget树的主要组成部分被分割成了非常简单的方法,这种方式的好处是纯粹和简单的代码可读性和可维护性,作为回报失去了优化性能,如果你想看更多内容,请转到底部的引用部分。

2、使用局部变量重构

和第一种重构方式有些相识,只不过这里使用局部变量,包括使用final变量初始化widget。在这里一样是将widget树的主要部分分割成多个,这增加了代码的可读性和可维护性。
在这种情况下,虽然我们的widget使用final来初始化变量,但是仍然使用的是父widget的BuildContext,当框架重绘父widget时,局部变量也将会被重绘。这增加了可读性和可维护性,你的widget树将会变浅,但是不会优化性能。

下面是一个带有常量的的重构代码示例:

final rowConstant = Row(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text(
          "Period",
          style: TextStyle(
            fontWeight: FontWeight.bold,
            color: Theme.of(context).textTheme.caption.color,
          ),
        ),
        SizedBox(
          width: 30,
        ),
        Text(
          "Last 30 days",
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 16,
          ),
        ),
      ],
    );

我们使用Visual Studio Code作为代码编辑器(AS也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选择 Refactor > Extract Local Varialble
4.在我们的例子中,将局部变量命名为rowConstant,注意我们使用final进行修饰,告诉Dart这是一个常量。
5.Row widget替换为了rowConstant最终变量。滚动带代码顶部,局部变量和widget都得到了很好的重构。
6.继续重构其他的Rows、Columns、Containers和Stack Widget。

3、使用widget class重构

这种方式允许你使用继承自StatelessWidget或者StatefullWidget的类,来隔离widget子树,还允许你创建可重用的widget,并且可以将它们分布在相同或不同的dart文件中,这样你就可以在程序的任何地方引入或者使用这些文件。警告!这些类的构造函数必须以const关键字开头,再次感谢Dart,以const开头声明的构造函数,会告诉Dart缓存和重用这些widget,与此相反的是其它widget将会被重绘。

当你要创建此类的对象时,不要忘记使用const关键字。通过这样做,当其他widget在widget树中更改状态时,此widget将不会被重建。如果遗漏了const关键字,父widget重绘多少次,我们的widget也将会跟着重绘多少次,因此需要留心。

这样的widget类依赖它自己的BuildContext,而不是像重构成方法或者变量的那样依赖于父widget的。BuildContext负责管理widget在widget树中的位置。

现在让我们看看使用这种方式的小例子:

class PaddingWidget extends StatelessWidget {
  const PaddingWidget({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 20),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              Text(
                "Period",
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Theme.of(context).textTheme.caption.color,
                ),
              ),
              SizedBox(
                width: 30,
              ),
              Text(
                "Last 30 days",
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 16,
                ),
              ),
            ],
          ),
          Container(
            height: 40,
            width: 40,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10),
              color: Theme.of(context).primaryColor,
              boxShadow: [
                BoxShadow(
                  offset: Offset(3, 3),
                  color: Colors.black12,
                  blurRadius: 5,
                ),
                BoxShadow(
                  offset: Offset(-3, -3),
                  color: Colors.white,
                  blurRadius: 5,
                )
              ],
            ),
            child: Icon(
              Icons.arrow_forward_ios,
              size: 14,
            ),
          ),
        ],
      ),
    );
  }
}

我们使用Visual Studio Code作为代码编辑器(AS也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选择 Refactor > Extract Widget
4.在我们的例子中,将类名命名为PaddingWidget
5.Padding widget替换为了PaddingWidget类。滚动带代码底部,类和widget都得到了很好的重构。
6.继续并重构其他Padding(PaddingWidgets class)、Rows(RowsAndColumnWidget class)widget。

抱歉,有太多内容需要消化,我总结一下:你不仅在可读性和可维护性上有所收获,并且性能也会有很大提升。因为当父widget重绘时,并不是所有widget都会被重绘,他们只构建一次。

结论

在这篇文章中,你了解到了widget树是widget嵌套的结果,随着widget的增加,widget树会迅速扩展并且降低代码的可读性以及可管理性,这被称之为整个widget树。为了提高代码的可读性和可管理性,你可以将widget分割成独立的widget类,创建一个浅的widget树。在每个程序中,你都应该尽量保持widget树层级浅。通过使用widget类的重构方式,你可以在Flutter子树的重构中获益,这将会提升性能。

感谢阅读我的文章,欢迎进行评论。

引用:
Beginning_Flutter

Refactoring a Flutter Project -- a story about progression and decisions

Refactorings and Code Fixes

JidiGutu/weather_neumorphism_ui

Flutter Community

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容