本文系本人于2016年创作于CSDN,最近迁移至此,今后文章修改都将提交于此 原文地址
前言
最近整理项目的代码,又回想到当初实习时一个人做公司项目的Android客户端,没有什么实践经验,又投路无门的状态,那一个月为了项目做的像点样子,又是在网上搜索,又是反编译别家的应用研究,真的时想破头皮。不过好歹,那段时间的折磨还是有那么点价值,现在想想,有必要整理总结,分享一下。
Android我也是看着网上的教材自学起来的,无论是Google 官方的 Training,还是各种教材,主要都着眼于各种控件和系统组件的使用,就像教小孩子玩积木说,这是长条,这是方块;诚然,这很重要,但是大部分培训和教材也就到此为止了,最多辅以一两个Demo一般的小练习,然后很多人就开始了找工作的碰壁之旅,这就像把积木都认识完了,只识得各种积木的孩子却要去搭一个摩天大楼,这个过程自然会比较艰辛。
这里我就整理一下我的自学经验分享出来,一方面是对自己工作学习的总结,另一方面也希望能够帮助到后来者 :)
最开始的思路来自于:Android应用架构 这篇文章,感谢作者和译者。
基本思路
“架构”指的是什么
在网上搜索Android架构相关的文章,一般会看到两种类型,一种常见起手是“MVP”、“MVVM”,然后甩一个实现了几个简单接口的demo过来;另一种起手就是“组件化”、“插件化”,举例动辄就是手淘等特大型电商App。
这两种都叫架构,但它们并不是一个层面的东西,前者是比较抽象的软件设计模式,后者则是针对超级App衍生的解决方案。可以这样说,一个插件化/组件化的大型App中的一个个小模块,其架构设计仍然是MVC/MVP/MVVM的。
一个Android初学者,从写完demo到开始做商业应用,首先遇到的就是比demo多N倍的功能和业务如何组织代码的问题,这时候就要去关注第一种“架构”。
移动应用类型
在说架构前,有必要了解一下我们所开发的应用类型作为基本思路,《App研发录》中对移动应用的类型划分我是比较赞同的,即一般的移动应用分为三种:数据展示类应用,手机助手类应用,游戏。
- 数据展示类:特点是页面多,需要频繁调用后端接口进行数据交互,以http请求为主;推送,IM类型App的IM核心功能以长连接为主,比较看重电量、流量消耗。
- 手机助手类:主要着眼于系统API的调用,达到辅助管理系统的目的,网络调用的方式以http为主。
- 游戏:一般分为游戏引擎和业务逻辑,业务脚本化编写,网络以长连接为主,http为辅。
一般我们做的App都是数据展示类,简要来说这类app的主要工作就是
- 把服务端的数据拉下来给用户展示
- 把用户在客户端修改的数据上传给服务端处理 当然一般应用上了规模也或多或少会涉及到系统API的调用,暂且不表。
原始的应用架构
其实在学完大部分Android课程之后,是有一个最基础的应用框架的,这个框架的核心,就是Activity和Fragment,一般你数据请求、缓存、编辑,UI控制,都在这两个类的实例里做,这也是在移动互联网初期市面上绝大部分应用的样子。
有人说Android应用的整体架构是MVC,M就是在应用中定义的Java Entities,V就是各种View,XML配置文件,C就是Activity和Fragment,但是在实际开发中,就会发现,Activity和Fragment简直是上帝一般的存在,它们不仅负责了Controller的功能,实际上也负责了View的管理,而且还持有诸如Context等系统资源。实际上可以把这个架构看作是一个MV之间相互交互的架构。
Activity/Fragment包办一切,这种组织形式很朴素,很直观,应用的整体代码组织以页面为区分,把需要复用的代码写几个工具类出来,应用模块划分就比较清晰了。如果业务不会持续增长上去,岁月静好,可是如果页面逐步复杂,这种原始架构的缺陷很快就会暴露出来:
首先是组件负担过重,一个购物车页面几千行代码,一个商品详情也是几千行代码,再网上写,编辑器都卡,找代码基本靠搜索,一个需求的实现散布在各个位置,增删业务都麻烦。公用的业务逻辑,写工具类太碎了,不写万一修改了又要改好几处。
其次是单元测试难,有些纯Java实现的东西可以用JUnit在电脑上自己过一遍,如果都写在Activity和Fragment里面就别想了。
一般来说,页面无关的方法,我们都会将其放到工具类里面,比如验证手机号、邮箱之类的正则,对Log的封装,而网络调用一般是通过异步网络库来做的,比如volley,Retrofit,封装为一个个Request,Call,通过将这些代码抽出来,会小幅改善Activity和Fragment压力过大的情况,(如何使用和封装这些网络框架请参见相关博文,此处不赘述也不写示例了
我做的第一个应用就是这样的架构,应用比较简单,但是写起来还是比较蹩脚,因为除了网络调用,Activity/Fragment还需要管序列化,操作本地数据库,操作本地文件等。比如获取当前用户信息的缓存,或者获取文章列表,购物车列表之类的数据,有可能涉及到多个页面使用数据,而获取数据也有一定的检查,比如分页加载和下拉刷新的判断,是否使用缓存等。
这些操作本身不应由Activity和Fragment来做,将这些操作放在网络模块或者Java Entities里面明显都不很合理。Java Entity本身就应该只是一个数据库数据映射的Java对象模型,赋予其管理缓存数据的职责只会让其变得混乱,功能不明确。比如涉及到分页加载的列表,如新闻列表,如果我在Model包中定义一个NewsList,那么这个NewsList到底是一个数据模型呢,还是一个数据管理者呢?想必以后看代码的时候,可能会困惑一下。User的数据一般是应用内全局使用的,如果我将其定义成一个单例模式,JSON反序列化之类的操作又会比较蛋疼了。而放在网络模块就更不合理了,为什么我注销用户会需要一个UserCall对象?
分层架构-原始架构的改良之路
在有了上面的困惑之后,改良的方案已经呼之欲出了: 抽象出一个新的管理者,让它去协调缓存与网络调用 。
其实在试图处理分页加载的数据缓存的时候,这个新的数据管理者就已经初步成形了,NewsList这个所谓的Model实际上就是一个数据管理者。只不过它的数据刷新需要依靠Activity/Fragment调用网络回调之后再set给它而已。抛开细枝末节仔细回想一下,从客户端的UI点击响应向服务端发起请求,到服务端返回数据刷新UI,其实是有一个清晰的数据流的:
UI发起请求 - 检查缓存 - 调用网络模块 - 解析返回JSON / 统一处理异常 - 反序列化 - 缓存 - UI展示
通过这个数据流,可以很明显的网络数据请求的一个三级分层:UI层,数据管理层(缓存处理,请求网络),网络调用层
继续以分页加载新闻列表这个例子来说:
之前只是声明了一个NewsList的Model,只有存储数据的功能,对于一个Java Entity来说,可能NewsList是这样的:
public class NewsList {
//当前新闻列表
private List<News> newsList = new ArrayList<News>();
//当前页码
private int currentPage = 1;
//总页码
private int totalPage = 1;
public NewsList() {
......
}
public void addToList(List<News> list, int currentPage) {
newsList.addAll(list);
this.currentPage = currentPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public void getTotalPage() {
return totalPage;
}
/* 以下各种set和get就不占篇幅了*/
}
既然这个看似Entity又不似Entity的对象负责了一部分数据管理的职责,那不如把数据加载的逻辑全部交给它,UI只需要调用它的接口获取数据不就好了?
我把这种本身贮存了UI直接使用的数据,且负责数据缓存加载的组件,叫做DataManager
通过DataManager的封装,可以将整个应用分为三层:UI,DataManagers,Network/LocalCache。
注意所谓分层并不是随便分几个package而已,而是有严格的职责和权限划分的,即每层都各有其职,每一层都向上层提供接口,封闭细节。
比如UI层向DataManager发起数据请求,应该只需根据用户端操作决定是否使用缓存,告诉DataManager,加载策略应该完全由DataManager控制,也不需要关心网络请求如何封装的报文参数。只需要根据DataManager提供的接口参数发起请求,即可获得数据。
现在不妨将NewsList提升为NewsListManager,也许它看上去就会更加合理了:
//首先定义一个获取数据的回调接口
public interface ActionCallbackListener<T> {
void onSuccess(T data);
void onFailed(Exception e, String message);
}
//Manager对象的实现
public class NewsListManager extends BaseDataManager {
//当前新闻列表
private List<News> mNewsList = new ArrayList<News>();
//当前页码
private int currentPage = 1;
//总页码
private int totalPage = 1;
public NewsListManager() {
getCacheFromDatabase();
}
public List<News> getCachedData() {
return mNewsList;
}
public void loadNewsList(boolean isRefresh, final ActionCallbackListener<List<News>> mActionCallbackerListener) {
if(isRefresh) {
clearCache();
currentPage = 1;
}
//假定这里是网络调用模块请求新闻列表的Request对象,细节不表
NewsListRequest request = new NewsListRequest();
request.setData(currentPage);
request.request(new RequestCallback() {
@Override
public void onSuccess(JSONObject response) {
if (reload) {
mNewsList.clear();
}
totalPage = response.optInt("total_page");
currentPage = response.optInt("current_page");
//将网络数据存储到manager.......
saveToDataBase()
if(mActionCallbackerListener != null) {
mActionCallbackListener.onSuccess(mNewsList);
}
}
@Override
public void onFailed(Exception e, String message) {
if(mActionCallbackerListener != null) {
mActionCallbackListener.onFailed(e, message);
}
}
});
}
private void getCacheFromDatabase() {
//将缓存数据从数据库中取出
}
private void saveToDatabase() {
//缓存新闻数据至数据库
}
private void clearCache() {
//清除数据库中的缓存
}
//......
}
//UI层调用
NewsListManager newsListManager = new NewsListManager();
NewsListAdapter adapter = new NewsListAdapter;
......
private void loadNewsList(boolean refresh) {
newsListManager.loadNewsList(refresh, new ActionCallbackListener<List<News>>(){
@Override
public void onSuccess(List<News> data){
adapter.notifyDataSetChanged;
}
@Override
void onFailed(Exception e, String message){
//show some hint
}
})
}
可以看到通过这样的封装,UI层根本不需要管理分页加载的逻辑,只需要调用NewsListManager的pageLoadNewsList()方法,告诉DataManager是否需要刷新即可,与UI的处理逻辑(下拉刷新,分页加载)一致,这样就极大的简化了Activity和Fragment的工作。同理,这样的逻辑也可以应用于应用使用的用户数据,通过isRefresh去判断是否需要从服务端重新拉取,因为大部分应用修改用户数据的入口就那么几个,大部分情况下是不需要每次请求用户数据都用request从网络获取的,DataManager实现的缓存机制就可以大幅减少不必要的接口调用,但是UI层请求数据的方法并没有任何改变。
一般把这种定义一个组件把数据管理剥离出Activity/Fragment的架构叫做“分层架构”。 这种架构大概是这个样子的: 由于绝大部分代码都从Activity和Fragment中剥离,现在Activity/Fragment只负责UI控制与数据获取,其它的绝大部分代码都可以做到UI无关,如果在开发中尽力确保这一点,那么在接口设计合理的基础之上,现有的DataManagers,网络模块以及其它一些工具类完全可以构成一个AppSDK,为手机,平板应用提供支持,项目无关的工具类和通用控件,则可以划归到公共开发资源库模块,大幅减少重复工作量。
分层架构的一些问题
在我近两年的工作中,使用分层架构有十分顺手,但有一个问题必须注意到,即DataManager的定义其实是很随意的,一个DataManager应该管理多少数据、几个接口、提供什么接口,都没有很明确的限制,这在多人合作的时候就比较容易出现DataManager设计思路放飞自我导致差异过大,DataManager的设计还是需要一定规范的。 另一方面,数据量起来之后,假如UI的状态受多个数据源的控制,填写UI数据的代码仍然会很庞大。这时候就需要去寻求更高一级的解决方案了。
MVP、MVVM-代码结构的高阶进化
MVC的C是即持有具体Model,又持有具体View,所以Controller很臃肿,分层架构就算抽出了DataManager,实质上仍然是一个MVC架构,View发送的指令和数据填充都是由Controller管理的。而MVP和MVVM则是Controller持有具体View这个问题做了点文章。
MVP架构
MVP就是将大量的原来由Controller管理的View数据填充与指令发送剥离出来交由Presenter,Presenter持有抽象的View;可以这么理解,Presenter是一个不管细节的Controller ~ 比如举例中的新闻列表,目前流行的MVP的View与数据交互方法可能是这样的
//抽象View定义
public interface INewsListView {
//分页加载数据
void onLoadNewsList(List<News>);
//某条新闻局部刷新
void onListItemUpdated(int position);
//某条新闻删除
void onListItemDeleted(int position)
}
// 抽象View实现
public class NewsListActivity extends BaseActivity implements INewsListView {
NewsListPresenter presenter;
NewsListAdapter adapter;
public void onCreate(Bundle savedInstances) {
......
presenter = new NewsListPresenter();
adapter = new NewsListAdapter(presenter.getNewsList());
......
listView.setAdapter(adapter);
// 向Presenter发起数据加载请求
presenter.loadNewsData();
}
@Override
void onLoadNewsList(List<News>) {
adapter.notifyDataSetChanged;
}
@Override
void onListItemUpdated(int position) {
adapter.notifyItemChanged(position);
}
@Override
void onListItemDeleted(int position) {
adapter.notifyItemDeleted(position);
}
}
如上所示,抽象的View将所有数据变更的逻辑封装了出来供UI实现接口,提供给presenter调用。
在我初次尝试的时候是在做电商应用,这种写法在一些简单的逻辑上看着挺漂亮,列表加载,登录,注册。但是一到了购物车,商品详情,结算这些复杂页面,就很莫名其妙了,以购物车为例:
购物车页面具有查(读)增删改(写)商品的功能,我以此为基准实现一个带有十几个方法的ICartView,购物车的Presenter叫CartPresenter。
如果我在结算页面想看购物车列表,想复用写在CartPresenter里面的方法,我要么实现ICartView,然后重写一堆空接口,要么定一个其它什么ICartReadView这种玩意。
同理可得,要么实现一堆丑了吧唧的空接口,要么把IXXXView拆的足够细,一次实现一堆接口
……
考虑了一下这样写的规模和修改的难度之后,我当场放弃,老老实实的写分层架构了……
后来Google出了TODO-MVP,但是发现跟上面那种Demo写法一样很麻烦,我也没有实际运用。后来反编译了某个大型App,发现其正好是MVP架构,于是仔细看了一下代码,就如同我最开始的想法,一个IXXXView接口有多少功能就写多少方法。再看看Presenter的实现,我忽然就明白我为什么会感觉不实用了:
任何想要构建一个其他什么东西取代Activity/Fragment地位的尝试都是自找麻烦
MVP正是一个典型
既然MVP把Activity/Fragment抽象为View,那么就意味着当它作为一个抽象View去使用的时候,生命周期,Context这些极其重要的资源Presenter是看不到的,但是这些东西是不可能不使用的。为了能让Presenter使用到这些,Presenter就必须持有Context,绑定Activity、Fragment的生命周期,就算如此,在一些需要确定使用Activity、Fragment的场合,仍需要使用强制转型。
这种IXXView、Presenter、Activity/Fragment高度关联的代码,改一次需求就要修改一大片,十分麻烦,而付出这么多代价所带来的好处仅仅只是在某些场景下多抽出了一点代码,以及应用看起来像MVP了……
目前我还没有在网上看到能够改变我以上看法的MVP架构demo,如果有小伙伴有比较好的实践经验,可以在评论告诉我。
MVVM架构
MVVM相比于MVP,最重要的一个概念就是“数据绑定”,Presenter还持有抽象的View,ViewModel连这个都不需要,双向绑定这个概念使得ViewModel更加轻量,复用性更强。
所谓双向绑定,在Android中就是View通过ViewModel订阅其所需的数据源,这个订阅的过程等同于set一个Listener;View持有ViewModel实例,ViewModel向View提供改变数据的接口,当View的操作引起数据改变或者数据源发生改变时,ViewModel通过回调告知View,View进行视图更新。
之前业界在数据绑定如何实现上一直没有特别稳定通用的方案,所以MVVM一直不温不火,在RxJava广泛应用于Android之后,略有起色,而Google I/O 2017放出了android-architecture-components,为MVVM架构提供了官方的ViewModel组件和LiveData组件,完美解决了这一问题
- ViewModel组件规范了ViewModel的所处地位,生命周期,生成方式,以及一个Activity下多个Fragment共享ViewModel数据的问题。
- LiveData组件则提供了在Java层面View订阅ViewModel数据源的实现方案,很轻量。
ViewModel的引入能够很好应对Activity销毁重建时大规模数据的恢复问题,以及多个界面依赖一个接口返回数据的场景,在这两个组件的规范下实现MVVM架构会十分容易,而且十分有意义。
由于我已经在项目中大规模使用了RxJava,因此数据绑定我是采用RxJava方案实现的。
关于使用 android-architecture-components 组件实现MVVM的方案可以参考
一点感想
不得不说,不需要优秀,哪怕一个靠谱的架构设计,都可以极大提升代码质量,降低工作量。这种改良版的应用架构目前在我的几个项目上都工作良好,极大减少了重复代码,不过最重要的一点是,即使在赶工阶段,已经基本什么都顾不上的状态下,依旧保持了应用整体的架构和规模没有失控,因为没有比利用框架提供的方式更快的写法。。。
总是有人说过早优化乃万恶之源,东西先出来再说,但我个人觉得无论是敏捷开发还是快速出东西,都是有门槛的,如果达不到这个门槛,为了速度而牺牲质量导致项目架构失控,代码混乱,结果就只有重构时更为辛苦,或者给别人留坑。效率为王这种概念,是针对熟练工说的,在没有经验的时候,多花点时间保证代码质量还是十分重要的。
一般来说我们做App,比如小外包,其实是用不到MVP,MVVM这样的架构的,一个分层架构就足以让我们快速高效的开发出App,选用什么框架,不仅要看你的应用类型,也要看你的应用规模,如果你有现成成熟的框架那无需多言,但如果你的应用只有几千行代码,为了追求MVVM,写了十几个类,踩了若干坑,只为了把一个Activity中的几十行代码抽到ViewModel里面,岂不是南辕北辙?
在分层架构的基础上,只要接口实现的足够好,代码够规范,切换到MVVM这样的架构也不是什么很难的事情。