重构如何改善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: [
            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: [
          Container(
            height: 100,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                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: [
                        Row(
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: [
                            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: [
                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: [
                      Align(
                        alignment: Alignment.center,
                        child: Icon(
                          Feather.loader,
                          size: 250,
                          color: Theme.of(context).accentColor,
                        ),
                      ),
                      Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: [
                          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: [
                                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: [
              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: [
                      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: [
                      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: [
          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: [
        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: [
        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: [
          Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              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

你可能感兴趣的:(重构如何改善Flutter应用程序的可读性、可维护性以及提升性能)