2020年终总结,一年来”磕磕绊绊“学习Flutter的成长之路

1,855 阅读16分钟

        2020年即将结束,可它注定是一个不平凡的一年。由于飞来横祸的疫情,打破了多少个原本一切正常的状态。看到了那么多因为疫情破产甚至不幸离世的消息,内心不满多了许多伤感。 每个人都因疫情或多或少受到了一些影响,只是冷暖自知不愿意多说罢了。在2020年即将结束的日子里不得不感慨时间过的好快,相信很多小伙伴跟我有同感。一年马上又要结束了,每个人都在成长,要说2020年让我成长的点,单对于工作技术方面应该就是学习Flutter了。

前言

        对于Android开发技术出身的我来说,必须得感慨一下在移动应用的市场上我只能为百分之50的人来做应用(另外的百分之50是iOS手机的的用户),并不能横跨移动端,这个事情一直是我心里的一个结(或许像我这样的只做Android开发或者iOS开发的同学们都会难免有这样的感触...)。 所以一直有计划想学习一门跨平台技术,但奈何自己的"事"还挺多。对于纯原生开发的来说,说实话并看不上那些类似于用网页嵌套在app实现的跨平台技术的框架,主要还是关于性能流畅度方面的因素。但对于RN的框架就不给予评价了,看到了《为什么Airbnb放弃了Rect Native?》这篇文章,关于维护成本高,难以维护,兼容性能的问题等等议论纷纷,就果断放弃了(只是个人观点,可能是自己太矫情,不喜勿喷)。那就学习iOS开发吧,这样可以横跨移动端。而对于Android开发的我来说,莫名的对iOS开发的学习有一种排斥感(哈哈哈,真的是"事"太多了), 还有一个更重要的原因是,本来用Android开发完了一款App,再让我开发一款同样的App在iOS手机上,内心难免有点抵触的,就这样计划一直被耽搁...

莫名的选择Flutter

在一次偶然的机会接触到了Flutter,看到了github上Flutter开源项目的活跃度如此之高,不免触动心弦。这样的增长速度位居其他框架的前列,有过之而无不及。

但要说对Flutter的选择用"莫名"的两个字概括真不为过。对于这样"事"的我来说,选择Flutter的原因有有三点:

  • 一套代码,横跨Mobile,PC端。
  • 身为Android开发,对Google的情怀。
  • 有独立的绘制引擎,性能流畅度方面上不亚于原生。

这样真的是可以完全实现跨平台了,出于好奇心抱着试一试的态度开始了尝试。

初写Flutter体验

在初写Flutter项目Demo时候,完全是一种懵逼的状态。以下实现一个"按钮"widget代码块为例:

  • 想让屏幕中显示一个Button的按钮,需要创建一个Button对象;
  • 想设置按钮的shape,需要创建RoundedRectangleBorder对象;
  • 想设置按钮的背景颜色,需要创建Color对象;
  • 想设置按钮的边框,需要创建BorderSide对象;
  • 想给按钮添加一个Widget,需要创建一个Text对象;
  • 想给Text设置大小,字体等,需要创建一个TextStyle对象;
ElevatedButton(
        onPressed: onTapCallback,
        child: Text(
          "Flutter Demo",
          style: TextStyle(
            color: Colors.black,
            fontSize: 17.0,
          ),
        ),
        style: ButtonStyle(
          side: MaterialStateProperty.resolveWith<BorderSide>(
            (states) => BorderSide(width: 1.0, color: AppColor.colorGreyBorder),
          ),
          backgroundColor: MaterialStateProperty.resolveWith<Color>(
              (states) => AppColor.colorBg),
          shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
              (states) => RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8.0),
                  )),
          elevation: MaterialStateProperty.resolveWith<double>((states) => 0.0),
        ),
      ),

这样的写法让我有点吃惊,很多疑问点冒了出来。为什么写一个Background,写一个Border,写一个Shape,甚至写一个文字的大小都需要创建对应的对象来进行设置? 如果按照这样的写法去写更复杂的页面就会展示出嵌套地狱这种式样了?对于没有接触过跨平台的初学者甚至有这样的疑问,关于调用手机本身功能(照片,定位等等)是如何实现的呢?在回答这些问题之前得先对Flutter有一些认知。

初识Flutter

Flutter简介

Flutter是Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。同时 Flutter还使用 Native引擎渲染视图,这无疑能为用户提供良好的体验。
Flutter通常被说成万物皆Widget,Widget中文意思是小部件。也就是说Flutter框架显示的UI是有多个小部件以树的形式呈现的。Widget包括按钮,菜单,同样也包括颜色,字体等等。简单总结一下就是 Flutter是有多个Widget来实现的,具有跨平台自绘引擎,支持多个平台,高性能等特性。

Dart简介

说到Flutter就不得不提一下Dart语言。因为Flutter是一个UI框架,它是基于Dart语言进行编写的。
Dart语言是谷歌于2011年开始发布的,发布这款开发语言的目的是为了克服JavaScript语言的缺点。 直白点说就是为了将JavaScript干掉。不过事与愿违JavaScript越来越火,而Dart逐渐消失在开发者的视角,直到Flutter的出现才重新回归舞台。
至于Flutter为什么选择Dart而不是JavaScript作为开发语言,这个就很有意思了,但也是很受争议和吐槽的点。其实无外乎重要的三点就是:

开发效率

Dart运行时和编译器支持Flutter的两个关键特性的组合:
基于JIT的快速开发周期:Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;
基于AOT的发布包: Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力;
这个优点其实也是用来弥补JavaScript的不足。(这里有必要感慨一下,并不是有一个比它好的语言就可以把它干掉还有额外很多其他的因素,但是不好的语言肯定不行。)

Dart的团队就在自己的身边

无关紧要,实则举足轻重。这样Flutter在处理问题点上可以获得最大的支持。如Flutter官网所述“我们正与Dart社区进行密切合作,以改进Dart在Flutter中的使用。例如,当我们最初采用Dart时,该语言并没有提供生成原生二进制文件的工具链(这对于实现可预测的高性能具有很大的帮助),但是现在它实现了,因为Dart团队专门为Flutter构建了它。同样,Dart VM之前已经针对吞吐量进行了优化,但团队现在正在优化VM的延迟时间,这对于Flutter的工作负载更为重要。”

为了让Dart依靠Flutter重新回归舞台,让人们认识。


Flutter在平台上呈现方式

      做过跨平台开发的同学应该清楚,写入同样的代码在移动端Android和iOS的端略有不同需要进行单独设配,其主要的原因还是它们都是通过原生组件进行交互的,这样必然会出现相同代码式样上略有不同的情况。
      Flutter的实现方式是将通过平台只需要提供一个Surface的画布,所有组件的呈现都是都通自己的独立的Skia渲染引擎进行渲染绘制的,因此无需担心式样适配的问题,一端开发另外一端可以完美呈现。
      至于如何实现拍照,定位等功能呢?因为Flutter只提供了绘制能力,为了方便和原生交互,Flutter提供了Platform Channel(平台通道)。也就是说Flutter一切和硬件适配交互的可以通过平台渠道去调用原生代码(Flutter社区提供了很多调用原生硬件功能的插件,可以直接使用,不需要再次实现)。

Flutter认识总结

在认识Flutter之后应该解决之前的疑惑吧。
1.组件是以树的形式呈现的所以需要这样的嵌套。
2.任何式样都需要相对应的对象,与其说对象不如说Widget更加合适。
3.Flutter应用可以通过Platform Channel进行与原生平台交互。至于嵌套地狱有能有办法解决呢?让我们来进一步学习Flutter。

优化Flutter代码

拿一个项目中Home页面的设计图分析一下,如果需要实现下方设计图的效果代码如下:

Column(
          children: [
            //实现头部代码块---------start--------------------------
            Container(
              width: double.infinity,
              height: MediaQuery.of(context).padding.top + kToolbarHeight,
              decoration: BoxDecoration(
                image: DecorationImage(
                    image: AssetImage("assets/bg_header3.png"),
                    fit: BoxFit.cover),
              ),
              child: SafeArea(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    GestureDetector(
                      behavior: HitTestBehavior.opaque,
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Image.asset(
                          "assets/icon_back.png",
                          width: 10.0,
                          height: 16.0,
                        ),
                      ),
                    ),
                    CommonText(
                      "Alunbrig Launch symposium",
                      fontSize: 18.0,
                    ),
                    GestureDetector(
                      behavior: HitTestBehavior.opaque,
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Image.asset(
                          "assets/icon_menu.png",
                          width: 14.0,
                          height: 10.0,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            //实现头部代码块---------end--------------------------
            SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  //实现轮播图代码块---------start--------------------------
                  AspectRatio(
                    aspectRatio: 375.0 / 211.0,
                    child: Stack(
                      children: [
                        PageView.builder(
                          controller: _pageController,
                          onPageChanged: (int index) {
                            _homeViewModel.setCurrentPageIndex(
                                index % _homeViewModel.videoCount);
                          },
                          itemBuilder: (BuildContext context, int index) {
                            return _homeViewModel.videoWidgetList[
                                index % _homeViewModel.videoCount];
                          },
                        ),
                        Center(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: [
                              GestureDetector(
                                behavior: HitTestBehavior.opaque,
                                child: Container(
                                  padding: const EdgeInsets.all(16.0),
                                  child: Image.asset(
                                    "assets/icon_left.png",
                                    width: 14.0,
                                    height: 27.0,
                                  ),
                                ),
                              ),
                              GestureDetector(
                                behavior: HitTestBehavior.opaque,
                                child: Container(
                                  padding: const EdgeInsets.all(16.0),
                                  child: Image.asset(
                                    "assets/icon_right.png",
                                    width: 14.0,
                                    height: 27.0,
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                  //实现轮播图代码块---------end--------------------------
                  
                  //实现内容布局代码块---------start--------------------------
                  Container(
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
                      alignment: Alignment.center,
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: List.generate(
                          6
                          (index) => Container(
                            margin: const EdgeInsets.symmetric(
                                horizontal: 8.0, vertical: 16.0),
                            decoration: BoxDecoration(
                              borderRadius: const BorderRadius.all(
                                  const Radius.circular(6.0)),
                              border:
                                  Border.all(color: Colors.black, width: 1.0),
                              color: Colors.black,
                            ),
                            width: 12.0,
                            height: 12.0,
                          ),
                        ),
                      )),
                  Padding(
                    padding: const EdgeInsets.only(
                        left: 20.0, right: 20.0, bottom: 50.0),
                    child: Column(
                      children: [
                        CommonText(
                          "イベント情報",
                          fontSize: 16.0,
                          fontWeight: FontWeight.bold,
                          txtColor: Colors.black,
                        ),
                        Container(
                          height: 1.0,
                          margin: const EdgeInsets.only(top: 8.0),
                          color: AppColor.colorGreyLine,
                        ),
                        GridView.count(
                          shrinkWrap: true,
                          crossAxisCount: 3,
                          crossAxisSpacing: 20.0,
                          mainAxisSpacing: 20.0,
                          childAspectRatio: 1.0,
                          physics: NeverScrollableScrollPhysics(),
                          children: List.generate(
                              itemInformation.length,
                              (index) => GestureDetector(
                                    onTap: () => _onClickItem(context, index),
                                    child: HomeItemWidget(
                                        title: itemInformation[index]["value"],
                                        imageUrl: itemInformation[index]
                                            ["key"]),
                                  )).toList(),
                        ),
                      ],
                    ),
                  ),
                  //实现内容布局代码块---------end--------------------------
                ],
              ),
            ),
          ],
        );

看到上边的代码,如果这个时候你还没有爆出口不得不佩服你的定力真好。反正我是忍受不了了,直接开喷Flutter真的垃圾,傻X式框架,这样写谁能受的了。当时果断放弃了学习Flutter的热情。 看到了网上喷Flutter的地狱式嵌套也在随声附和叫好。
就这样Flutter的学习计划暂停了一段时间,当有一天看到了关于介绍Flutter的文章,内容大概写着"Flutter的一切都是Widget,而Widget做到的是一切皆颗粒化。" 看到这里想到了为什么我不能把写的Flutter的代码尽可能做到颗粒化呢,这样嵌套地狱就迎刃而解了。有的时候就是这样,不要花费很长的时间去纠结某件事情。也许当你再一次回头看的时候会想的很明白。

按照这个思路对上图设计的代码进行了调整:

  • 头部Widget
  • 轮播图Widget
    • 轮播图控件PageView
    • 轮播图封面控制组件OverlayViewPageWidget
    • 录播图指示器IndicatorViewPageWidget
  • 内容Widget
    • 内容Widget的头部部分
    • 内容Widget的GridView
      • GridView的item部分

将Home页面分为三个Widget模块的布局代码:

Column(
          children: [
            //实现头部代码块---------start--------------------------
            HeaderWidget(
              onPressCallBack: () {
                Navigator.of(context).pop();
              },
              openMenuCallBack: () {
                // _showMenuDialog(context);
              },
            ),
            //实现头部代码块---------end--------------------------
            SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  //实现轮播图代码块---------start--------------------------
                  ViewPageWidget(model),
                  //实现轮播图代码块---------end--------------------------
                  //实现内容布局代码块---------start--------------------------
                  Padding(
                    padding: const EdgeInsets.only(
                        left: 20.0, right: 20.0, bottom: 50.0),
                    child: Column(
                      children: [
                        CommonText(
                          "イベント情報",
                          fontSize: 16.0,
                          fontWeight: FontWeight.bold,
                          txtColor: Colors.black,
                        ),
                        Container(
                          height: 1.0,
                          margin: const EdgeInsets.only(top: 8.0),
                          color: AppColor.colorGreyLine,
                        ),
                        GridView.count(
                          shrinkWrap: true,
                          crossAxisCount: 3,
                          crossAxisSpacing: 20.0,
                          mainAxisSpacing: 20.0,
                          childAspectRatio: 1.0,
                          physics: NeverScrollableScrollPhysics(),
                          children: List.generate(
                              itemInformation.length,
                              (index) => GestureDetector(
                                    onTap: () => _onClickItem(context, index),
                                    child: HomeItemWidget(
                                        title: itemInformation[index]["value"],
                                        imageUrl: itemInformation[index]
                                            ["key"]),
                                  )).toList(),
                        ),
                      ],
                    ),
                  ),
                  //实现内容布局代码块---------start--------------------------
                  
                ],
              ),
            ),
          ],
        );

将轮播图模块分解布局代码:

Column(
      children: [
        AspectRatio(
          aspectRatio: 375.0 / 211.0,
          child: Stack(
            children: [
              //pageView组件--------------start----------
              PageView.builder(
                controller: _pageController,
                onPageChanged: (int index) {
                  _homeViewModel
                      .setCurrentPageIndex(index % _homeViewModel.videoCount);
                },
                itemBuilder: (BuildContext context, int index) {
                  return _homeViewModel
                      .videoWidgetList[index % _homeViewModel.videoCount];
                },
              ),
              //pageView组件--------------end----------
              //OverlayViewPageWidget组件--------------start----------
              _OverlayViewPageWidget(goViewPageCallBack: () {
                _pageController.jumpToPage(_homeViewModel.getGoPageIndex());
              }, backViewPageCallBack: () {
                _pageController.jumpToPage(_homeViewModel.getBackPageIndex());
              }),
              //OverlayViewPageWidget组件--------------end----------
            ],
          ),
        ),
        //IndicatorViewPageWidget组件--------------start----------
        _IndicatorViewPageWidget(widget.model),
        //IndicatorViewPageWidget组件--------------end----------
      ],
    );

        通过上边对Widget的细化,会发现根本不会出现嵌套地狱的说法。之所以出现嵌套地狱,代码杂乱不可阅读还是因为写代码规范的问题所在。 在这里在强调说一下写的Widget尽可能细化是Flutter官方提倡的做法。为什么这样说呢?因为Flutter本身是响应式开发,它是以树的形式找到当前节点对该节点和所有的子节点进行响应刷新页面修改式样的。 如果你没有细化你的Widget,那么只要是有一点轻微的改动就会进行大面积的Widget的build。这样的做法是不可取的,因为会影响性能。比如按照设计图上的来说,我只是修改了HeaderWidget里边的文字,那么为什么后两个Widget模块也要跟着刷新重新build,这样是不合理的。

开源首个Flutter处女座项目

        就这样磕磕绊绊的学习Flutter一段时间。因为对于响应式开发并不了解,所以在开始的时候根本理解不了Flutter的刷新机制,还是按照Android开发的逻辑思维进行编写Flutter代码。 这让我处处碰壁,不得不说在思维转换痛苦的过程中度过了一段时间。 我一直认为好记性不如烂笔头,学习东西不要只停留在学上,要把学到的东西应用在实践中。于是就开始筹划开发一个免费的app,因为当时在用一款"88影视"的app看一些免费的视频,但对于这款app真的无力吐槽了, 它用的是网页编写打包成app的,性能差的一批。在有就是各种各样的花式广告真的令人心烦。所以决定仿照重新开发高性能,无广告的同款。通过抓包软件先将api给全部抓取,之后进行Flutter版本的开发。
        在开发的过程中会发现"你以为你都学会了,其实才学到了皮毛",各种问题迎面而来。其实这是必然的结果,因为在学习中你只是进行片段学习,你并不会整合在一起,也不会在实践中知道如何处理问题等等, 总之实践真的很重要。就这样边开发边学习遗漏的知识点,用了将近一个月的业余时间初步开发完成。
        在开发中我一直用Android进行调试的,开发完成之后尝试用iOS手机运行时,看到画面用两个字来形容就是"完美",真的是毫无式样差别,令我不得感叹一下Flutter是真的香。 这里有必要提一下,同等条件下用Flutter开发应用的周期比用原生开发应用的周期提升百分之25,甚至会更高,这个结论是在我实践中得出来的。

关于个人开源的"帅帅影视"的这个项目就不在这里细说了,如有兴趣可以看一下下面的链接。

"帅帅影视"App的介绍
"帅帅影视"github的地址

Flutter的不足

        说了这样多,可能有些人会说Flutter的真的完美,无懈可击。其实当然不是这样的,它也有它的不足。我们要知道没有任何事情是绝对的,它既然有优点当然也会有缺点,它们俩会同时存在。 那么来简单说一下Flutter有哪些不足呢?

  • 1、混合开发的问题表现,因为前面说过Flutter本身在跨平台开发的实现就是创建了一个Surface的画布进行绘制布局的,这就导致混合的页面栈的管理困难,维护成本大。再有就是WebView和键盘的问题也一直让人头疼(不过这块Flutter官方也一直在优化中...)。
  • 2、没办法进行热更新。这归结于Flutter进行AOT打包后产物是二进制文件不符合平台的动态化。(这个确实就是没办法的事情了,因为无论是Google还是Apple都不提倡进行热更新,甚至Apple已经铭文禁止,因为这样更新App是不可控的)。
  • 3、App的体积和内存消耗大。这归结于Flutter提供的独立的渲染引擎skia,肯定会导致内存消耗大一些。其次就是在Android上因为系统本身就支持Skia所以比较好,但是在iOS需要把Skia引擎也打包进App里,所以体积会变大不少(Flutter也一直在努力优化skia引擎)。

如何学习Flutter

在回答这个问题之前,先了解一下Flutter目前的近况如何,这里采用了@恋猫de小郭的总结对大厂使用跨平台技术在知名app上的使用情况。

从上边的图片我们可以看出,已经有很多知名app接入Flutter了,Flutter比RN和Weex出来的晚,但使用程度上已经快超过了,由此可见Flutter受欢迎的程度还是很可观的。
那么如何学习Flutter呢?我的学习步骤是这样的:

  • 一、Flutter本身提供的Widget很多,也包括现有的各种原生的控件,所以我们要去熟悉和会使用这些常用的组件。
  • 二、当你对于Flutter提供自身的组件有一定了解之后,要去学习一下Flutter的原理和刷新机制,这为了你在项目中写良好高性能的代码打下好的基础。
  • 三、去了解和学习个人、社区、官方为Flutter提供的知名的插件。
  • 四、关注一些Flutter大神写的博客和开源项目。

关于第四点补充一下比较不错的链接,这些多的学习Flutter的帮助还是蛮大的。

Flutter中文网
老孟程序员
XuYisheng
恋猫de小郭

总结

        身在科技高速发展的时代,前端框架层出不穷,对于前端工程师来说不得不感慨学不完啊。所以对于学习技术的方向一定要有一个想法,我并不认为Flutter作为跨平台技术的框架会是最好的, 但至少在目前来说还是一个不错的选择。其实对于跨平台的技术应该去学习一下,至于学习哪个技术就要在于自己的想法,公司的需要,市场的走来想综合考量了。最后提前祝同学们新年快乐,祝愿在新的一年有更好的成长。最后祝愿疫情可以早点结束!