有一说一,Android主题的经验分享已经很久没有更新了。但是!我还是在做Android开发的,而且相比于两年前,现在对于Android开发的理解也有了质的飞跃。
为了证明这点,这节课我们就来讲讲Android架构(architecture)
一个容易上手的Android项目应该如何搭建?
可能对于许多学过Android课程的人来讲,这也是个很难回答的问题。因为课程上很少有人会跟你讲“架构”二字。可能很多(30~40%?)会Android开发的人还是使用着最基本的MVC模式,用着google support依赖,使用线性布局和相对布局的嵌套来书写布局…或者还有更极端的人用着ADT Bundle,最低API适配到9。对于一个现代Android应用来说,开发者应该做到与时俱进,而这一切的资料,其实都在developer.android.google.cn
有着详细的说明。
Android的架构,其实是一种组织代码的形式。不过在这篇文章我们先不讨论xml
文件的组织,我们只讲java
代码的组织。
如何组织代码
如何组织代码,这是一个问题。
首先大家都知道面向对象的编程模式(OOP),在很多情况下,我看到别人写的代码是这样的:在一个类的代码中书写这个类应该要干的事情。
听起来是不是没什么毛病?其实这样会导致很多的问题。
举个例子,假如一个类非常庞大,它需要负责UI的控制功能,又要去向服务端或者本地数据库获取数据,并且这是一个比较复杂的页面,本身需要管理的控件就很多…
这时候可能就有人问了,那我要是把这个页面拆小,分而治之呢?
想屁吃呢,这样组织的代码只会增加耦合,降低内聚。正常的做法应该是去拆分UI控制、数据获取,并且能够从一定程度上再去整理一下,让代码变得可复用。(这就涉及到设计模式了,可以自行百度,等我讲到这个可能要很久以后了)
写过Android的同学可能就有感觉了,在Android开发中,Activity和Fragment的代码往往会非常长,而且方法查找也非常不方便(即使用double shift查找也很不方便),这就是Android默认项目架构没有给我们解决的问题。它只是抛出来了一个原始MVC的模型,而且有一说一这个模型很难工程化。
但是!Google官方给聪明的开发者留了一个好东西——Android架构指南 GitHub传送门
这个仓库有很多的分支,有MVVM和MVP的,还包含了Java\Kotlin版本,以及RxJava,LiveData,DataBinding,mock的使用,可以说你总能挑到一个方便你转型的版本(我用的是todo-mvp分支)
好了,下面开始围绕官方的架构指南来进行讲解。
MVVM模式
MVVM
全称Model-View-ViewModel
,听上去非常蛇皮,但是做Web开发的同学对这个词一定不陌生。MVVM模式可以让开发者更少地关注数据绑定,也就是如何把数据放到组件上显示。最简单的例子就是写Vue的时候在html代码中的双大括号:
1 | <div id="text">{{tvContent}}</div> |
这样写的好处是:你只管处理数据就好了,当数据被set到data中之后的事情都交给框架处理。
在Android中,你也一样可以使用MVVM简化数据绑定(即使是RecyclerView)。当然这是因为官方给我们封装好了数据绑定的库,通过xml的声明即可实现视图和数据的双向绑定。实现的方式在官方文档里面写得很详细:
在Android项目中实现MVVM
完整代码:项目传送门
按照Google给出的demo,对于每一个功能模块,目录结构如下(假设模块名为test,单页面):
1 | class TestActivity : Activity主体,layout框架,实现TestNavigator |
首先Activity和Fragment好理解,就不展开了。主要就说一说ViewModel的设计。
TestViewModel继承自ViewModel,ViewModel不是包含在依赖中的,而是需要通过Factory来创建。可以全局定义一个ViewModelFactory类,具体实现见demo 精准空降。ViewModel的作用是和数据源(Repository)进行双向的交互(获取/修改),并且隐式操作Data Binding库(demo中使用LiveData所以就是修改LiveData对象)来实现View的更新,所以可以看到图上画的是一个虚箭头。
MVVM模式的缺点
下面这段话摘自官方仓库的README:
当应用程序修改MVVM体系结构中的ViewModel时,库或框架会自动更新View。您无法直接从ViewModel更新视图,因为ViewModel无法访问必要的引用。
但是,您可以在MVP架构中从Presenter更新视图,因为它具有对视图的必要引用。当需要更改时,可以从Presenter显式调用View进行更新。在此项目中,您将使用布局文件将ViewModel中的可观察字段绑定到特定的UI元素,例如TextView或ImageView。数据绑定库确保View和ViewModel双向保持同步,如下图所示。
这也就解释了为什么会多出来UserActionListener类和Nagigator类来辅助执行用户事件监听和页面路由。
我当时就是看到了这段话…欸这不是在说MVP的好嘛,那我就用MVP了。(过于真实)
MVP模式
为什么使用MVP,很大一部分原因是MVP就是从Java MVC中衍生出来的。它是为Java量身定制的一个架构。(所以.NET也由此启发用了MVP [手动狗头])
让我们回顾一下,在Android中为什么MVC不好用。这要从一个问题开始说起:
Activity的职责是什么(不引入Fragment)?
答案上面也有,主要是UI操作、数据交互、应用内部通信。设想,如果没有Fragment,这么多的代码统统塞到Activity里面,那这样的代码有多难维护想想也知道了。但是这个问题单单引入Fragment是解决不了的,就像上面提到的耦合和内聚的问题,正确的解决方法还是把这些职责剥离开来实现。
MVP好处都有啥?这是来自百度百科的解释:
MVP与MVC的主要区别是View与Model不直接交互,而是通过与Presenter来完成交互,这样可以修改视图而不影响模型,达到解耦的目的,实现了Model和View真正的完全分离。视图的变化总是比较频繁,将业务逻辑抽取出来,放在表示器中实现,使模块职责划分明显,层次清晰,一个表示器能复用于多个视图,而不需要更改表示器的逻辑(当然是在该视图的改动不影响业务逻辑的前提下),这增加了程序的复用性。数据的处理由模型层完成,隐藏了数据,在数据显示时,表示器可以对数据进行访问控制,提高数据的安全性。以前的Android开发是难以进行单元测试的,但是随着项目变得复杂,测试时保证应用质量的关键,MVP模式中,表示器对视图是通过接口进行的,可以利用测试驱动,模拟出视图对象,实现视图相对于表示器的接口,就可以对表示层进行不依赖于UI环境的单元测试了,这大大降低了Android应用开发中的业务逻辑测试难度和复杂度。MVP模式的引入,视图层完全不依赖与模型层,相当于将视图从特定的业务场景中脱离出来,做到了对业务完全不可知的状态,因此可以将视图层组件化,提供一系列接口供表示层操作,这样就可以做出高度可复用的视图组件了。
在这里我就不开小标题了,直接在这里贴仓库和架构图
完整代码:项目传送门
与MVVM的思路不同,MVP的P,是Presenter(呈现者)。Presenter扮演的角色是和数据源/View的双向交互。从架构图看来,和MVC是相同的,但是这个Presenter巧在什么地方呢?就在它的设计。
Presenter有两种实现方法,一种是Presenter + PresenterImpl,一种是Contract + Presenter。一般我们推荐使用Contract,Contract就是契约的意思,它规定了View和Presenter都设计了哪些方法来实现业务逻辑。
Contract示例代码如下:
1 | public interface TasksContract { |
可以看到Contract没有任何内部定义的方法,只有两个接口View和Presenter,而Fragment(真正的View层)实现了Contract.View,Presenter实现了Contract.Presenter,由于是双向的交互,所以Fragment(或者说View)和Presenter都持有彼此的实例,这样达到View中触发Presenter,Presenter更新View。
其实这里还有一个细节,还记得上文提到的问题吗?
“在Android开发中,Activity和Fragment的代码往往会非常长,而且方法查找也非常不方便(即使用double shift查找也很不方便)”
有了Contract之后,因为它是interface,你可以不去关心它怎么实现(只要命名和注释达意到位),而你要增加或者删除一个方法,对应的代码里面会直接报红(增加话是implements这一行包红波浪线,删除的话是对应的@Override注解报红),然后你就可以马上定位到并进行修改了。太tm方便了!
这样一来一个功能模块的基本结构就很简单了:
1 | class TestActivity : Activity主体,layout框架 |
关于MVP的介绍安利就到这里了,更多的可以自己clone官方仓库的分支去理解理解。
数据源 - Repository
解决了Controller层的问题,我们再来思考一下,Model层应该如何组织。一般来说Model层只是放置了POJO类型,可能更完善一点,涉及到数据库操作,那会再增加一个DAO层。但是对于真正的应用来说,这点是完全不够的,因为还要考虑通过网络请求的方式去服务端获取数据。
写过RESTful API的应该都知道联网的数据操作和DAO其实是有点相似的,DAO是决定执行哪条语句,REST是决定请求哪个API。
插播:众所周知Android有两大网络请求库:OkHttp和基于OkHttp的Retrofit。(其实都是square出品)
不妨再去回顾一下MVVM和MVP的架构图,其实数据源部分是一样的。仔细看发现demo中的data包里面其实就包含了这些(哎呦我滴妈还有意外收获)
和Contract类似,这里也有一个对interface的巧用:DataSource类
1 | public interface TasksDataSource { |
这个接口定义了由谁来实现呢?一共有3个实现:TasksRepository、TasksLocalDataSource、TasksRemoteDataSource。
Local和Remote很好理解,Local对应的就是执行DAO要干的事情,Remote对应的就是执行网络请求。而Repository同时拥有这两个类的实例,目的是协调什么时候让本地去做,什么时候让远程去做,或者干脆一起做,然后做完了再去执行一个回调(这个在下一节会讲)。注意这3个实现的类都是以单例模式运行的。
Java线程通信 - Callback机制
1 | public interface RUOKCallback{ |
你在说你马呢,这写的是个啥
什么是Callback,为什么要用Callback。
比如你要寄一封信,去邮局投递了之后,你总不会在邮局待着不走等邮局回复是吧,这么长的时间你肯定要去干自己的事情。而对于邮局而言,他要是把你的信件弄丢了,那也会收到差评影响生意,所以邮差在把信件送到目的后,会告诉邮局我这封信送到了,丢了也不是我的锅。这时候邮局再根据你留下的手机号发一条短信告诉你信件送到了。
是不是很流畅?
当执行耗时操作时,我们希望用一个工作线程去执行这个操作,主线程(UI线程)继续干别的事情,只不过操作完成后主线程还要负责向用户展示结果是否成功。这样我们就提出了一个多线程的概念。在架构指南中,Google官方推荐我们开3个Executor,分别处理主线程、DiskIO、NetworkIO。
多线程实现了,如何实现线程通信呢?故事中,邮局告诉你信件送达是一个线程通信,其实还有一个,邮差和邮局之间也是线程通信。这种通信方法我们称之为Callback,回调。
如何实现Callback呢?你需要这几步:
- 定义Callback(最好是
interface
,用匿名实例去实现,比较方便) - 定义方法时使用Callback作为参数
- 方法需要回调时执行对应的Callback方法(如果是UI更新需要移步主线程)
简单的例子:(Callback使用上面那个)
1 | public class A { |
写了3个小时终于写好了,又可以愉快地回去搬砖了。
长按点赞三连!三连!!连!!!