如何用Flutter从0开始搭建一个App
如何用Flutter从0开始搭建一个App
本文将介绍一种适合初学者的Flutter App开发流程。与传统的瀑布模型不同,这种流程从UI开发开始,逐步实现业务逻辑和底层服务,特别适合小型项目和独立开发者。
最近有几个朋友加我微信,上来就问有没有实战课程。后来聊了一下才知道他们都自学过Flutter或者客户端开发,但是碰到搭建一个规模相对比较大的App的时候还是会觉得有些地方不知道从何下手。这种情况其实也很好理解,比如我们学习Flutter,主要的注意力还是放在各种组件、如何搭建UI这方面。但是当一个App的规模稍微大了一点,就会出现很多UI之外的东西,比如可能需要用到对接后台服务器、本地数据库,如果规模再大一些还需要划分不同的模块甚至子系统,模块和子系统之间可能还需要定义接口甚至通信协议。对一个初学者来说如果一上来就要解决这么多问题,确实是有些困难的。
正好前段时间帮人开发了一个规模不算大的App,使用Flutter开发,Dart的代码量在1~2w的样子,页面有十来个。总体来说是一个小型的App,但是包含了服务端对接、本都数据库、三方库的封装定制等功能,算是一个麻雀虽小五脏俱全的项目。最重要的是我在这个项目中尝试了一种新的开发模式,我觉得非常适合初学者从头开始掌握一个App开发的方方面面。
1、标准开发流程
我们先来说一下比较标准的开发流程。据我所知目前国内大多数团队还是基于瀑布模型简单变化,有少数团队使用敏捷开发,但是真敏捷的不多。本文不是讲瀑布、敏捷的,所以这里就简单提一下,主要是为了方便后面的对比。
比如我们要新开发一个App:
一、要对需求做一个评估
这时会把功能在前后台做一个划分,这一步在和产品经理对需求的时候基本上就能确定下来了。
二、转换为软件功能模块
第一步生成的功能列表主要还是偏向于用户视角的功能。这一步会把这个功能列表拆分成开发人员需要开发的功能模块列表。
三、根据功能模块做架构设计
因为是一个新开的App,各种基础设施都是空白,所以架构设计大概率不能省。这一步我们会拆分出大家比较熟悉的服务端对接模块、数据库模块、页面管理模块、状态管理、日志模块等等。讲究一点的还会做架构分层,比如基于MVC、MVP、MVVM等进行分层。
四、代码开发
其实按照瀑布模型的标准,前面那一步基本上可以算作概要设计,接下来应该是详细设计,比如画个类图、数据流图等等。但是据我所知这一步在很多公司都是省略的。所以到这里基本上就是把不同模块分给不同的人,然后就开始干活了。
如果你现在的团队流程和上面说的差不多,我强烈建议你在第四步考虑使用TDD(测试驱动开发)。因为在第四步的改变不涉及研发之外的其它团队,研发小组内就可以推动。我的经验是切换到TDD后bug率会有50%左右的下降,同时对整个开发进度的掌控力会有大幅的提升。
好了,前面就是对比较常见的开发流程做一个简单的说明。如果是超大型公司的复杂流程,比如搞CMMI的,或者敏捷团队可以忽略。其实大家可以看到,这套流程对新人或者个人开发者不是太友好。那么接下来我就说说我尝试的新流程,能帮助新人快速进入状态。
2、直接开始写代码
先说明一下这个流程其实主要基于敏捷开发做了简化,对初学者和中小型项目比较适用,大型项目目前还没有尝试过。和前面的标准流程相比,从第二步开始就不一样了。
第一步,仍然是需求评估
第二步,区分出UI功能
这里不需要再拆分出多个功能模块了,只需要拆分成UI功能和非UI功能即可。
第三步,开始写代码
是的,你没看错,这里就开始写代码了。这里我们会从UI开始,只写UI代码。也就是说我们会把第二步拆分出来的UI功能优先实现。
为什么一定要从UI开始?
有两个原因:
首先,在这个阶段,UI部分是功能最明确、最清晰也最容易进入开发的部分。
其次,UI可以作为思考整个项目的锚点。这句话怎么理解呢?对于初学者和独立开发者而言,很容易陷入到某一个技术细节中难以跳出来。这时如果App的主体页面都已经有了,而且能互相跳转,你就有了一个实实在在可以看见的东西。这个东西能够展示App对用户呈现的大部分状态,或者说这就是一个点击按钮能跳转到对应页面的App,只不过有些功能还不完善。那么,有了这个App你就相当于把凭空构造一个东西的作文题变成了在一个App上添加一点功能的填空题。相信我,这个心态的转变会让你对整个项目的掌控感有本质的提升。
那么UI要开发到什么程度呢?简单来说,设计图里面的东西,只要没有特殊原因都实现了。换个说法,就是用户能看到的东西都要实现。
比如:
- 页面启动后需要从服务端加载数据,你可以用一个Future延时200ms后返回一个写死的固定数据
- 用户在登录页输入用户名、密码后点击登录按钮,可以直接跳转到登录成功页或者主页
- 所有的跳转、弹窗等等都要实现
总的来说就是在没有实现业务逻辑的情况下也让这个App能跑起来,看起来像真的一样。
这里面有一个地方比较特殊,就是状态管理。
页面的状态管理
我们用一个商品详情页举例。
一般来说,商品信息是从服务端取回来的。那么这个页面至少有三种状态:
- 数据加载中:展示加载动画或者提示(一般刚进入页面就是这个状态)
- 数据加载成功:展示商品详情
- 数据加载失败:展示加载失败的提示
这是一个典型的使用页面状态管理的场景。这里我们不用考虑从服务端获取数据的场景,可以简单的使用Future返回固定数据。这时可以分为两种情况:
如果你是纯新手,单纯的画UI对你来说都有一定的难度,那么可以先不考虑页面状态管理的问题。把每种状态需要展示的UI封装成一个组件或者一个函数,比如Loading、Detail、Error三个组件。然后手工切换展示不同的组件,来查看UI效果。然后等UI工作完成后,把增加状态管理作为一个单独的任务来完成。
如果你已经有一定的经验或者熟练度,画UI对你来说不需要占用全部的大脑带宽,这时可以选择一个状态管理方案集成到你的UI体系中。我一般选择状态管理的原则是这样的:
- 如果页面状态都很简单,比如就像前面的商品详情页,只有三个状态,那么就选择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的实例。
在这个bloc中我们可以实现对服务端商品接口的调用。如果这是你第一个调用服务端接口的页面。
那么不用想太多,你可以放心的把所有调用服务端接口的功能都写到这个文件中。这里通常我们至少会封装一个方法,比如就叫getDetail。方法中你可能会基于Dio实现http功能。传入url和接口要求的参数后就能获取到对应的商品信息了。然后把服务端返回的数据通过异步的返回值返回给调用者。bloc调用getDetail并获取到异步的返回值后就会更新自身的状态并通知订阅者。比较常见的订阅者是BlocBuilder。我们实现BasePage定义的buildPage方法,会返回一个组件树。这个组件树就是页面的内容,而BlocBuilder通常会再组件树中随着状态变化的那一层级。BlocBuilder收到状态变化通知后就会调用自己的回调更新自身的子树,最终完成页面内容的更新。
好的,到这里为止,我们的商品详情页就初步开发完成了。接下来你就可以按照这个思路去开发其它页面了,直到碰到第二个需要调用服务端接口的页面,就进入第五步。
第五步,抽象出各种服务
当我们碰到第二个页面页需要调用服务端接口时,如果你仍然按照第四步的思路,会发现需要写很多重复的代码。这里你应该升起一种警觉:在软件开发中,重复是原罪。为什么要强调这样一个浅显的道理?因为我的职业生涯中,见过太多的垃圾代码都是复制一大坨过来,改几个参数了事。
那么这个时候,我们应该做的就是把重复的代码抽象出来。继续那服务端接口来说。我们假设第二个调用服务端接口的页面获取的是用户信息。你封装了一个方法叫getUserInfo。你会发现这个方法和前面的getDetail有很多内容是重复的,只是传入的url以及参数不一样。它们返回的数据可能都是json格式的。那么这个时候即使你是第一次做开发,也大约能够想到,这里需要把服务端接口的调用抽象出来。
我们可以首先定义一个工具类,比如ServerApi。然后在里面定义一个方法比如loadServerData。这个方法接收两个参数,url和接口调用参数列表。返回一个json数据。方法中通过dio实现真正的http调用。我们可以先在商品详情页的getDetail方法中调用这个loadServerData。等验证整个流程都没有问题后,就可以在其它页面使用ServerApi了。
这里还有一个小细节。如果你对接的是比较正规的服务端接口。那么你在getDetail和getUserInfo两个方法调用loadServerData时,会发现大量的重复参数。我们通常把一个服务端上不同接口都需要传递的参数叫做公共参数。如果每次调用loadServerData都需要把所有的公共参数重新生成一遍然后传入明显是不合理的。所以我们应该把公共参数的生成和传递给http库封装在loadServerData方法内部。
好的,前面我们以调用服务端接口为例。说明了如何抽象出一个系统的底层工具模块,以及抽象的时机。如果你的App还有其它同层级的模块,比如本地数据库、IM实时通信、视频播放等等。都可以按照这个思路逐步的完成抽象封装工作。最终你的整个项目会得到一个相对完整的架构体系,比如下面这样。
总结
总结一下整个开发流程。
当有了一个需求,我们并没有遵循传统的模式进行架构和详细设计。而是直接从UI部分进行开发。当整个UI部分开发完成后,再逐步填充每个页面底下的业务逻辑。如果业务逻辑用到了更底层一些的服务比如调用服务端接口,我们也不是一次设计到位。而是随着开发的推进,逐步采用重构的方式抽象出更底层的服务。
之所以我要专门花时间测试这样一套开发流程。是觉得相比于一上来就把整个App的框架设计明白。这个方案对初学者和很多个人开发者更友好,更容易找到一个切入点。很多个人开发者都有现成的App整体框架可以直接在新项目中套用,这当然是更高效的一种模式。另一方面,前面介绍的这套流程如果能配合TDD,其实是对项目质量和掌控力更高的方案。但是本来这就是面对初学者的,如果叠加TDD,可能反而给初学者造成更多的负担了。
好了,关于如何从0开始开发一套App的具体流程就先介绍到这里。如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。