头图

哈喽,我是老刘

最近有几个朋友加我微信,上来就问有没有实战课程
后来聊了一下才知道他们都自学过Flutter或者客户端开发
但是碰到搭建一个规模相对比较大的App的时候还是会觉得有些地方不知道从何下手

这种情况其实也很好理解
比如我们学习Flutter
主要的注意力还是放在各种组件、如何搭建UI这方面
但是当一个App的规模稍微大了一点,就会出现很多UI之外的东西
比如可能需要用到对接后台服务器、本地数据库
如果规模再大一些还需要划分不同的模块甚至子系统
模块和子系统之间可能还需要定义接口甚至通信协议
对一个初学者来说如果一上来就要解决这么多问题,确实是有些困难的

正好前段时间帮人开发了一个规模不算大的App
使用Flutter开发,Dart的代码量在1~2w的样子,页面有十来个
总体来说是一个小型的App
但是包含了服务端对接、本都数据库、三方库的封装定制等功能
算是一个麻雀虽小五脏俱全的项目

最重要的是我在这个项目中尝试了一种新的开发模式
我觉得非常适合初学者从头开始掌握一个App开发的方方面面

1、标准开发流程

我们先来说一下比较标准的开发流程
据我所知目前国内大多数团队还是基于瀑布模型简单变化
image.png
有少数团队使用敏捷开发,但是真敏捷的不多
本文不是讲瀑布、敏捷的,所以这里就简单提一下,主要是为了方便后面的对比
比如我们要新开发一个App
一、要对需求做一个评估
这时会把功能在前后台做一个划分,这一步在和产品经理对需求的时候基本上就能确定下来了
二、转换为软件功能模块
第一步生成的功能列表主要还是偏向于用户视角的功能
这一步会把这个功能列表拆分成开发人员需要开发的功能模块列表
三、根据功能模块做架构设计
因为是一个新开的App,各种基础设施都是空白,所以架构设计大概率不能省
这一步我们会拆分出大家比较熟悉的服务端对接模块、数据库模块、页面管理模块、状态管理、日志模块等等
讲究一点的还会做架构分层,比如基于MVC、MVP、MVVM等进行分层
四、代码开发
其实按照瀑布模型的标准,前面那一步基本上可以算作概要设计,接下来应该是详细设计
比如画个类图、数据流图等等
但是据我所知这一步在很多公司都是省略的
所以到这里基本上就是把不同模块分给不同的人,然后就开始干活了

如果你现在的团队流程和上面说的差不多
我强烈建议你在第四步考虑使用TDD(测试驱动开发)
因为在第四步的改变不涉及研发之外的其它团队,研发小组内就可以推动
我的经验是切换到TDD后bug率会有50%左右的下降
同时对整个开发进度的掌控力会有大幅的提升

好了,前面就是对比较常见的开发流程做一个简单的说明
如果是超大型公司的复杂流程,比如搞CMMI的,或者敏捷团队可以忽略

其实大家可以看到,这套流程对新人或者个人开发者不是太友好
那么接下来我就说说我尝试的新流程,能帮助新人快速进入状态

2、直接开始写代码

先说明一下这个流程其实主要基于敏捷开发做了简化
对初学者和中小型项目比较适用,大型项目目前还没有尝试过
和前面的标准流程相比,从第二步开始就不一样了

第一步,仍然是需求评估

第二步,区分出UI功能

这里不需要再拆分出多个功能模块了
只需要拆分成UI功能和非UI功能即可

第三步,开始写代码

image.png
是的,你没看错,这里就开始写代码了
这里我们会从UI开始,只写UI代码
也就是说我们会把第二步拆分出来的UI功能优先实现

为什么一定要从UI开始?
有两个原因:
首先,在这个阶段,UI部分是功能最明确、最清晰也最容易进入开发的部分
其次,UI可以作为思考整个项目的锚点
这句话怎么理解呢?
对于初学者和独立开发者而言,很容易陷入到某一个技术细节中难以跳出来
这时如果App的主体页面都已经有了,而且能互相跳转
你就有了一个实实在在可以看见的东西
这个东西能够展示App对用户呈现的大部分状态
或者说这就是一个点击按钮能跳转到对应页面的App,只不过有些功能还不完善
那么,有了这个App你就相当于把凭空构造一个东西的作文题变成了在一个App上添加一点功能的填空题
相信我,这个心态的转变会让你对整个项目的掌控感有本质的提升

那么UI要开发到什么程度呢?
简单来说,设计图里面的东西,只要没有特殊原因都实现了
换个说法,就是用户能看到的东西都要实现
比如:
页面启动后需要从服务端加载数据,你可以用一个Future延时200ms后返回一个写死的固定数据
用户在登录页输入用户名、密码后点击登录按钮,可以直接跳转到登录成功页或者主页
所有的跳转、弹窗等等都要实现
总的来说就是在没有实现业务逻辑的情况下也让这个App能跑起来,看起来像真的一样
image.png

这里面有一个地方比较特殊,就是状态管理
页面的状态管理
我们用一个商品详情页举例
image.png
一般来说,商品信息是从服务端取回来的
那么这个页面至少有三种状态

  • 数据加载中:展示加载动画或者提示(一般刚进入页面就是这个状态)
  • 数据加载成功:展示商品详情
  • 数据加载失败:展示加载失败的提示

这是一个典型的使用页面状态管理的场景
这里我们不用考虑从服务端获取数据的场景,可以简单的使用Future返回固定数据

这时可以分为两种情况:
如果你是纯新手,单纯的画UI对你来说都有一定的难度
那么可以先不考虑页面状态管理的问题
把每种状态需要展示的UI封装成一个组件或者一个函数
比如Loading、Detail、Error三个组件
然后手工切换展示不同的组件,来查看UI效果
然后等UI工作完成后,把增加状态管理作为一个单独的任务来完成

如果你已经有一定的经验或者熟练度,画UI对你来说不需要占用全部的大脑带宽
这时可以选择一个状态管理方案集成到你的UI体系中
image.png
我一般选择状态管理的原则是这样的
如果页面状态都很简单,比如就像前面的商品详情页,只有三个状态
那么就选择Provider
如果有的页面状态比较复杂,就选择Bloc

选择好状态管理方案后,需要简单的封装一下
比如定义一个所有页面的基类BasePage,在里面封装Bloc

abstract class BasePage<B extends BaseBloc> extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<B>(  // BlocProvider除了为子树提供Bloc,也负责页面关闭后调用Bloc的dispose方法释放资源
      create: createBloc,
      child: buildPage(context),
    );
  }

  B createBloc(BuildContext context);

  Widget buildPage(BuildContext context);
}

这样所有的页面都继承这个基类,并实现对应的方法就可以使用状态管理了

这里只是提供一个思路,具体的状态管理库和封装方案,大家可以根据自己的喜好选择

好的,到这里为止,你已经完成了UI的开发工作
也就是说现在已经有一个可以运行的App了,并且能进行简单页面跳转和交互
那么下一步就是把这个App的各个功能补全

第四步,实现业务逻辑

仍然以前面举的例子商品详情页来说
这里要实现的业务逻辑就是真正的调用服务端接口
还记得我们前面在UI部分已经完成了状态管理的开发
比如我们选择使用bloc
那么我们会在这个页面实现基类定义的createBloc方法
该方法应该去创建这个页面对应的bloc
而我们已经在BasePage中封装了BlocProvider
所以在这个页面的子树的任何位置,我们都可以很方便的获取这个bloc的实例
image.png
在这个bloc中我们可以实现对服务端商品接口的调用

如果这是你第一个调用服务端接口的页面
那么不用想太多,你可以放心的把所有调用服务端接口的功能都写到这个文件中
这里通常我们至少会封装一个方法,比如就叫getDetail
方法中你可能会基于Dio实现http功能
传入url和接口要求的参数后就能获取到对应的商品信息了
然后把服务端返回的数据通过异步的返回值返回给调用者

bloc调用getDetail并获取到异步的返回值后就会更新自身的状态并通知订阅者
比较常见的订阅者是BlocBuilder
我们实现BasePage定义的buildPage方法,会返回一个组件树
这个组件树就是页面的内容,而BlocBuilder通常会再组件树中随着状态变化的那一层级
image.png
BlocBuilder收到状态变化通知后就会调用自己的回调更新自身的子树,最终完成页面内容的更新

好的,到这里为止,我们的商品详情页就初步开发完成了
接下来你就可以按照这个思路去开发其它页面了
直到碰到第二个需要调用服务端接口的页面,就进入第五步

第五步,抽象出各种服务

当我们碰到第二个页面页需要调用服务端接口时
如果你仍然按照第四步的思路,会发现需要写很多重复的代码
这里你应该升起一种警觉:在软件开发中

重复是原罪
重复是原罪
重复是原罪

为什么要强调这样一个浅显的道理?
因为我的职业生涯中,见过太多的垃圾代码都是复制一大坨过来,改几个参数了事

那么这个时候,我们应该做的就是把重复的代码抽象出来
继续那服务端接口来说
我们假设第二个调用服务端接口的页面获取的是用户信息
你封装了一个方法叫getUserInfo
你会发现这个方法和前面的getDetail有很多内容是重复的,只是传入的url以及参数不一样
它们返回的数据可能都是json格式的
那么这个时候即使你是第一次做开发
也大约能够想到,这里需要把服务端接口的调用抽象出来

我们可以首先定义一个工具类,比如ServerApi
然后在里面定义一个方法比如loadServerData
这个方法接收两个参数,url和接口调用参数列表
返回一个json数据
方法中通过dio实现真正的http调用

我们可以先在商品详情页的getDetail方法中调用这个loadServerData
等验证整个流程都没有问题后,就可以在其它页面使用ServerApi了

这里还有一个小细节
如果你对接的是比较正规的服务端接口
那么你在getDetail和getUserInfo两个方法调用loadServerData时,会发现大量的重复参数
我们通常把一个服务端上不同接口都需要传递的参数叫做公共参数
如果每次调用loadServerData都需要把所有的公共参数重新生成一遍然后传入明显是不合理的
所以我们应该把公共参数的生成和传递给http库封装在loadServerData方法内部

好的,前面我们以调用服务端接口为例
说明了如何抽象出一个系统的底层工具模块,以及抽象的时机
如果你的App还有其它同层级的模块,比如本地数据库、IM实时通信、视频播放等等
都可以按照这个思路逐步的完成抽象封装工作
最终你的整个项目会得到一个相对完整的架构体系,比如下面这样
image.png

总结

总结一下整个开发流程
当有了一个需求
我们并没有遵循传统的模式进行架构和详细设计
而是直接从UI部分进行开发
当整个UI部分开发完成后,再逐步填充每个页面底下的业务逻辑
如果业务逻辑用到了更底层一些的服务比如调用服务端接口
我们也不是一次设计到位
而是随着开发的推进,逐步采用重构的方式抽象出更底层的服务

之所以我要专门花时间测试这样一套开发流程
是觉得相比于一上来就把整个App的框架设计明白
这个方案对初学者和很多个人开发者更友好,更容易找到一个切入点
很多个人开发者都有现成的App整体框架可以直接在新项目中套用,这当然是更高效的一种模式
另一方面,前面介绍的这套流程如果能配合TDD,其实是对项目质量和掌控力更高的方案
但是本来这就是面对初学者的,如果叠加TDD,可能反而给初学者造成更多的负担了
好了,关于如何从0开始开发一套App的具体流程就先介绍到这里
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》


程序员老刘
1 声望2 粉丝

客户端架构师,客户端团队负责人。一个月带领客户端团队从0基础迁移到Flutter 。目前团队已使用Flutter五年。