“ 本文是Shorey软件开源系列的第二篇,上一篇主要介绍了产品相关,本篇是对Flutter从零开发一款App的简单介绍 ”
大家新年好,距离上篇文章发布已经过了一个多月了,Flutter团队也发布了最新的Flutter 2.8稳定版本,我也升级了自己用的SDK版本到2.8,并且做了一些代码层面的重构,目前Shorey 0.2.0beta已经发布,欢迎在以下地址试用:
- Github:github.com/elementlo/s…
以下我主要分享一些如果想从头开始用Flutter写一款自己的App需要了解的东西,着重点会主要在工程方面,不会深入Flutter的技术细节,主要目的是给早已对Flutter跃跃欲试或者感兴趣的人一个参考。
01
—
准备工作
开发环境
-
操作系统: Mac/Windows(以Mac为例)
-
Flutter SDK: flutter.cn/docs/get-st… 下载
.cn域名是由Flutter中文社区维护的镜像网站,定期会同步Flutter官网的内容,其他也有很多镜像站,不要尝试。选择最新稳定版下载,解压缩。
-
镜像设置(可选):
设置替换以下两个环境变量:
$ export PUB_HOSTED_URL=https://pub.flutter-io.cn$ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn设置完记得source ~/.bash_profile保存下。替换环境变量是为了后续拉取Flutter的package时不会因为国内网络问题被阻塞。\
其他相关资源的镜像地址可以从这里查询:\
stats.uptimerobot.com/JZK3ZTql79
-
集成开发工具:
1. Android Studio (推荐)
2. Visual Studio Code
有关开发工具的更详细教程可以参考这里:
-
检查开发环境
当你完成上面的配置后,可以在命令行敲下flutter doctor命令去检查一下自己的环境,有问题的地方顺着指引去修改就好。
02
—
从Shorey入手
在正式开发App之前,容我用最简单的语言概括下Flutter是什么,以及用它能做什么。
- Flutter是Google在2015年推出的跨移动端和桌面端的UI开发框架,它使用Dart开发语言,拥有独立的图形渲染引擎Skia,力求通过一套代码解决UI一致性问题。
接下来就可以把Shorey的代码clone下来或者自己从编译器新建项目了,需要说明的是Flutter项目目前可以分为三类,新建的时候可以选择:
-
Application:一个可以部署到设备上的应用程序
-
Plugin:包含设备原生代码(Android/iOS)可以集成进Applicaiton里的插件项目
-
Package:纯Dart代码的插件
本次主要介绍第一种也就是App的开发,Shorey就是一款App。
代码架构
当建好项目,你首先要做的就是决定要使用哪种代码架构去组织自己的软件:熟悉客户端开发的同学知道有MVC,MVP,MVVM,以及现在流行的MVI架构;熟悉Web开发的同学也会对MVC,MVVM有所耳闻,更多的前端框架帮忙处理了。这些架构的主要作用是分离UI和业务逻辑,起到解耦的作用,不同的地方在于业务中数据的响应方式(单向流动/双向流动),以及配合这种响应方式控制代码的组织方式。由于Flutter天生是命令式的前端框架(参考了React),使用数据驱动UI变化,所以天然具有MVVM的部分优势,因此Shorey选择了封装官方推荐的Provider组件来实现一个简单的MVVM架构。
Note:不用太纠结于架构和框架的选择,你可以完全不使用他们或者选择GetX,Bloc,fish_redux的任意一种,他们并不会让你的应用更加好用。
关于Provider的详细使用方式可能会专门开一篇来介绍,这里你只要知道Provider封装了Flutter的InheritedWidget,可以在数据发生变化时自动通知UI刷新界面。你可以直接复制Shorey项目base包里的代码到自己项目下,然后参考Shorey的使用方式使用这种模式。
这里简单介绍下使用这种模式数据的流转方式,如下图:(图片来自网络)
这里的ViewModel就是抽离了逻辑代码的部分,数据的操作和改变都发生在这里,相应的当改变之后需要通知订阅了自己的UI组件刷新。相当于实现了一套简单的观察者模式,熟悉Android Jetpack ViewModel的同学应该对这种封装很有既视感。
采用这种模式构建的项目,一般意义上来说每个“Page”就对应着一个ViewModel,部分数据需要在整个App内流转(用户信息),可以使用一个全局的ViewModel来控制。这也代表着Flutter中的StatefulWidget的大部分功能,除了生命周期相关,都可以使用ViewModel来实现,这对页面的性能调优也能带来一些方便,这放在下一章节介绍。
Note:Provider可以作为App的根节点或者页面的根节点接入,但是对于跨页面传值这种场景没有很方便的使用方法,官方推荐的是复用注入的provider。这里建议还是使用全局的provider来传值。
第一个页面
Dart语言和其他很多语言一样,应用程序使用main函数作为入口,在main函数中会调用Flutter Framework提供的runApp方法来初始化Flutter应用,这里边包括绑定到Flutter引擎,创建配置树Widget Tree,节点树Element Tree以及渲染树RenderObject Tree。最后通过布局,绘制,合成把几何图层交给图形引擎Skia加工成二维图像数据,并最终交给GPU进行渲染,我们就看到了屏幕上的界面。
实际上,在Flutter世界中,一切皆是Widget,大到App的页面,小到设置Widget的paddings,都是用“Widget”实现。Widget主要分为两类:StatelessWidget和StatefulWidget,怎么选择?我总结了下:当Widget渲染出来后不需要再响应任何交互,无论是主动交互——比如用户操作,还是被动交互——比如刷新倒计时,可以使用无状态控件——StatelessWidget,反之,就使用StatefulWidget。这两点可能需要在实践中再琢磨琢磨就能明白。
这就引出上面提到的页面性能优化的问题,用Provider封装的ViewModel的出现让StatelessWidget也有了“状态管理”的能力,这就扩展了StatelessWidget的使用场景,因为StatelessWidget的状态是在构造函数调用时就确定的,后续不再发生改变,这就显著降低了页面的刷新频率,提高了性能,如果需要StatelessWidget中的状态发生变化,可以使用上面提到的provider来管理,使用provider中的context.watch()/read(),或者consume() API来控制刷新粒度,做到局部刷新的效果。
Note:尽管Flutter引擎的刷新控件效率很高(时间复杂度大致为O(n)),频繁刷新界面也看不到任何负面影响,但是在业务逐渐庞大复杂之后,特别是异步的网络请求加进来之后,还是会对应用流畅度造成可观的影响。
路由
Flutter的路由使用起来很简单,分为命名路由和一般路由,命名路由需要提前在App的路由表里注册,一般路由则直接使用类名跳转。
但是真正使用起来会觉得不方便的地方很多,可定制化的地方很少,另外Flutter团队虽然在前段时间发布了Navigation 2.0,重构了路由,使之也能像写声明式页面一样写路由,但是要想使用重构的成本非常高,代码量也急剧提高,不推荐。2.0是对对路由这块有非常高的自定义需求的新应用准备的。
Note:Flutter中的路由跳转并不是由类似子Tree创建这样的方式实现的,也并没有直接对Widget操作,而是使用Overlay的机制实现的,所以类似Provider这种向父节点查找的操作是找不到的。
数据
一般应用中的数据,根据来源区分,主要有来自网络的和来自本地的,因为目前Shorey是完全的本地App,所以着重介绍下本地数据这块,网络请求会放在下个章节简单介绍。
本地数据根据存储介质可以分为缓存和持久化存储,在Shorey中也分出了两个管理类来分别操作不同的数据,缓存层封装使用了sembast,持久化存储层使用drift数据库实现。
缓存没有使用官方的shared_preferences有两个原因,一是因为sp安卓的实现还是SharePreference类,它对于异步操作以及频繁的读写支持的都不太好,如果是安卓原生开发大都转移到使用Google的DataStore了;二是因为Shorey的存储场景更适合NoSQL:需要频繁的拉取更新数据,不需要很强的事务能力,所以选择了sembast,也就是缓存的数据全部来自于内存,在应用初始化时就会从磁盘取出所有的缓存数据放入内存中,保证使用时能快速响应。
持久化存储选用了drift数据库框架也就是原来的moor,drift最近也被Flutter官方评为Flutter Favorite包。其主要优势在于扩展了很多级联操作,你能想到的基本都有Api支持,而且是不用手写SQL语句的。缺点是学习门槛比较高,你需要首先熟悉整个框架是怎么运作的,然后再去细细地看它的Api文档,好在他的文档比较详尽。
Note:需要注意的一点,Sqlite数据库在3.6.19中才加入对外键的支持,并且默认是关闭的,需要主动打开才能使用。
上面说的两个数据层对应的源码是DataProvider和DbProvider,可以对照Shorey中实例来了解它们是怎么使用的。
网络
目前Flutter上使用最广泛的网络库是dio,我想作者一定也是JOJO迷。dio在一般情况下已经足够好用,Api书写起来也很舒服,可以直接使用。如果你有很高的自定义通用拦截器需求或者App内的网络接口环境十分复杂,比如需要定制不同接口有不同的域名,不同的header等情况可以自己封装一层,使用多个dio实例来配置不同的接口请求。在其他项目中,我是这么做的。
界面
在第二节中简单介绍了下Flutter中的“Widget”,这一节对开发UI中容易忽略的细节做一个总结:
-
页面的root节点一般都是Scaffold,它会提供一个页面基本的骨架,使整体风格符合Material/Cupertino风格,并附加必要的Padding。
-
如果对Flutter中Widget的显示尺寸、位置感到迷惑,可以先看看官方总结的顺口溜:
意思是:一个Widget先问问它的parent有什么约束(Constraints),然后遍历它的孩子问问他们在这个约束下你们想要什么怎样的空间尺寸(Sizes),并把这个尺寸报告给他们的parent,最后由parent把这些孩子一个个摆放在合适的位置。
-
创建Widget最好(必须)都放在build方法里,由Flutter Framework来维护它们的生命周期,因为每一个Widget都可能会延伸出一颗大“树”,这些野“树”你驾驭不住。
-
最后来说一点代码美观的东西,Flutter采用类似React的嵌套构造函数写法来描绘界面,当页面层级比较浅时没什么问题,一旦深了之后就出现“嵌套地狱”了。
解决方法也很简单:
(1) 提取某些共有的控件形成方法;
(2) 提取某些共有的控件形成新的类;
上面代码摘自Shorey的类别列表页,提取CategoryItem后会有所改善。
这两种方法都可以,从Google官方的Demo来看,官方更喜欢第二种方法。还有一些稍微高阶的用法,比如使用Dart 2.6中加入的扩展函数功能,为控件编写扩展功能,也能达到效果。
其他
开发过程中可能还会遇到其他方面的问题,比如国际化,打包,异常捕获等,由于篇幅问题就不展开聊了,推荐一下Google官方的示例App-Gallery,使用了各种Flutter的前沿特性,相信能学到不少东西。
03
—
后续Shorey规划
Shorey发布了0.2.0beta版本,增加了类别的自定义功能,主要也是为了后边整合Notion做准备。后续会投入进集成Notion的开发,目标是把Shorey作为Notion的中间层,在Shorey上添加的内容可以自动同步到Notion中,日积月累的沉淀出你自己的WIKI。
04
—
总结
本篇主要结合Shorey介绍了下Flutter App的整个开发,大部分地方都只是泛泛而谈,目的是想让感兴趣的人有个大致的了解,可以独立开发一款App。其中的知识点每一个拎出来都够一篇了,后续会慢慢都更新出来。另外最近因为业务需要对Flutter动态化和AOP也做了些研究,也想分享下经验。
目前已经有很多优秀的博主在讲解Flutter源码、剖析原理了,我主要想从工程角度多谈谈感受和经验,希望可以帮助到有想法但没入坑的朋友踏出第一步。
因为不想随便写些零散的东西出来糊弄,下一篇可能要等到Shorey接入Notion的开发完成之后了,我们下篇文章见。