Android 从 Demo 到商业项目的转变 - 应用架构设计

1,826 阅读15分钟
原文链接: blog.csdn.net

最近整理项目的代码,又回想到当初实习时一个人做公司项目的Android客户端,没有什么实践经验,又投路无门的状态,那一个月为了项目做的像点样子,又是在网上搜索,又是反编译别家的应用研究,真的时想破头皮。不过好歹,那段时间的折磨还是有那么点价值,现在想想,有必要整理总结,分享一下。

Android我也是看着网上的教材自学起来的,无论是Google 官方的 Training,还是各种教材,主要都着眼于各种控件和系统组件的使用,就像教小孩子玩积木说,这是长条,这是方块;诚然,这很重要,但是大部分培训和教材也就到此为止了,最多辅以一两个Demo一般的小练习,然后很多人就开始了找工作的碰壁之旅,这就像把积木都认识完了,只识得各种积木的孩子却要去搭一个摩天大楼,这个过程自然会比较艰辛。

这里我就整理一下我的自学经验分享出来,一方面是对自己工作学习的总结,另一方面也希望能够帮助到后来者 :)

ps: 最近网上流行MVP和MVVM,Google官方也推出了MVP的Sample:Android-Architecture,不过网上大部分Demo级别的写法我在尝试运用到项目时遇到了很大困难,Google-MVP也没大规模应用过,以后再分享学习心得。

ps: 最开始的思路来自于:Android应用架构 这篇文章,感谢作者和译者。


0. 基本思路

首先要了解一下我们一般做的商业应用都是什么,看过《App研发录》对于其中移动应用的类型划分是比较赞同的,即一般的移动应用分为三种:数据展示类应用,手机助手类应用,游戏。
数据展示类应用特点是页面多,需要频繁调用后端接口进行数据交互,一般都涉及支付流程,考验弱网络环境下数据的正常获取,减少电量和流量消耗。
手机助手类应用则主要着眼于系统API的调用,达到辅助管理系统的目的。
我们一般做的应用都是数据展示类型应用,然而一般应用上了规模也或多或少会涉及到系统API的调用,暂且不表。

1. 从0开始的基本版应用框架

其实在学完大部分Android课程之后,是有一个最基础的应用框架的,这个框架的核心,就是ActivityFragment,有人说Android应用的整体架构是MVC,M就是在应用中定义的Java Entities,V就是各种View,XML配置文件,C就是Activity和Fragment,但是在实际开发中,就会发现,Activity和Fragment简直是上帝一般的存在,它们不仅负责了Controller的功能,实际上也负责了View的管理,而且还持有诸如Context等系统资源。实际上可以把这个架构看作是一个MV之间相互交互的架构

刚刚开始做Demo一般也是先从页面开始,一个页面建一个Activity或者Fragment,然后初始化数据,初始化控件,绑定监听......所有的这些操作都是在Activity和Fragment定义的类里面进行的。对于一些只有几个输入框,或者只有一个ListView的Demo来说,这样写并不会有什么问题。
但是,假如现在要做的不是Demo,而是一个商城应用的购物车页面呢?或者是商品详情页面,订单结算页面呢,这些页面都有大量的控件初始化,数据增删改查,手势操作,如果这些事情都交给Activity/Fragment来做,那么一个类的代码很快就会破千行。涉及到诸如用户数据之类的应用全局共享数据,又或是检查缓存之类的功能,Activity/Fragment的负担也会十分繁重。

对于这种问题,解决方法就是要分担Activity和Fragment的工作,一般来说,页面无关的方法,我们都会将其放到工具类里面,比如验证手机号、邮箱之类的正则,对Log的封装,而网络调用一般是通过异步网络库来做的,比如volley,Retrofit,封装为一个个Request,Call,通过将这些代码抽出来,会小幅改善Activity和Fragment压力过大的情况,(如何使用和封装这些网络框架请参见相关博文,此处不赘述也不写示例了)
整个项目大概的结构是这个样子的:

我做的第一个应用就是这样的架构,应用比较简单,但是写起来还是比较蹩脚,因为除了网络调用,Activity/Fragment还需要操作当前的Java Entity对象,操作本地数据库,操作本地文件等。比如获取当前用户信息的缓存,或者获取文章列表,购物车列表之类的数据,有可能涉及到多个页面使用数据,而获取数据也有一定的检查,比如分页加载和下拉刷新的判断,是否使用缓存等。这些操作本身不应由Activity和Fragment来做,将这些操作放在网络模块或者Java Entities里面明显都不很合理。Java Entity本身就应该只是一个数据库数据映射的Java对象模型,赋予其管理缓存数据的职责只会让其变得混乱,功能不明确。比如涉及到分页加载的列表,如新闻列表,如果我在Model包中定义一个NewsList,那么这个NewsList到底是一个数据模型呢,还是一个数据管理者呢?想必以后看代码的时候,可能会困惑一下。User的数据一般是应用内全局使用的,如果我将其定义成一个单例模式,JSON反序列化之类的操作又会比较蛋疼了。而放在网络模块就更不合理了,为什么我注销用户会需要一个UserCall对象?

2. 基本版架构的改良之路

在有了上面的困惑之后,改良的方案已经呼之欲出了:抽象出一个新的管理者,让它去协调缓存与网络调用
其实在试图处理分页加载的数据缓存的时候,这个新的数据管理者就已经初步成形了,NewsList这个所谓的Model实际上就是一个数据管理者。只不过它的数据刷新需要依靠Activity/Fragment调用网络回调之后再set给它而已。先抛开细枝末节仔细回想一下,从客户端的UI点击响应向服务端发起请求,到服务端返回数据刷新UI,其实是有一个清晰的数据流的:
UI发起请求 - 检查缓存 - 调用网络模块 - 解析返回JSON / 统一处理异常 - JSON对象映射为Java对象 - 缓存 - UI获取数据并展示

通过这个数据流,可以很明显的网络数据请求的一个三级分层:UI层,数据管理层(缓存处理,请求网络),网络调用层 
继续以分页加载新闻列表这个例子来说:
之前只是声明了一个NewsList的Model,只有存储数据的功能,对于一个Java Entity来说,可能NewsList是这样的:
  1. public class NewsList {  
  2.     //当前新闻列表  
  3.     private List<News> newsList = new ArrayList<News>();  
  4.     //当前页码  
  5.     private int currentPage = 1;  
  6.     //总页码  
  7.     private int totalPage = 1;  
  8.   
  9.     public NewsList() {  
  10.         ......  
  11.     }  
  12.   
  13.     public void addToList(List<News> list, int currentPage) {  
  14.         newsList.addAll(list);  
  15.         this.currentPage = currentPage;  
  16.     }  
  17.   
  18.     public void setTotalPage(int totalPage) {  
  19.         this.totalPage = totalPage;  
  20.     }  
  21.   
  22.     public void getTotalPage() {  
  23.         return totalPage;  
  24.     }  
  25.     /*以下各种set和get就不占篇幅了,这里也有一个上面所述的问题,即对于一个数据管理者来说, 
  26.         要开放get/set把私有数据给外部用么?如果不开放,它是Entity么?这也是职责混乱的一个体现吧...*/  
  27. }  
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就不占篇幅了,这里也有一个上面所述的问题,即对于一个数据管理者来说,
		要开放get/set把私有数据给外部用么?如果不开放,它是Entity么?这也是职责混乱的一个体现吧...*/
}

现在不妨将其提升为NewsListManager,也许它看上去就会更加合理了:
  1. //首先定义一个获取数据的回调接口  
  2. public interface ActionCallbackListener<T> {  
  3.     void onSuccess(T data);  
  4.   
  5.     void onFailed(Exception e, String message);//这里异常返回可以是处理过的异常id,也可以是原始的Exception对象,按照自己设计即可  
  6. }  
//首先定义一个获取数据的回调接口
public interface ActionCallbackListener<T> {
	void onSuccess(T data);

	void onFailed(Exception e, String message);//这里异常返回可以是处理过的异常id,也可以是原始的Exception对象,按照自己设计即可
}
  1. //Manager对象的实现  
  2. public class NewsListManager extends BaseDataManager {  
  3.     //当前新闻列表  
  4.     private List<News> mNewsList = new ArrayList<News>();  
  5.     //当前页码  
  6.     private int currentPage = 1;  
  7.     //总页码  
  8.     private int totalPage = 1;  
  9.   
  10.     public NewsListManager() {  
  11.         getCacheFromDatabase();  
  12.     }  
  13.   
  14.     public List<News> getCachedData() {  
  15.         return mNewsList;  
  16.     }  
  17.   
  18.     public void pageLoadNewsList(boolean isRefresh,  final ActionCallbackListener<List<News>> mActionCallbackerListener) {  
  19.         if(isRefresh) {  
  20.             clearCache();  
  21.             mNewsList.clear();  
  22.             currentPage = 1;  
  23.         }  
  24.         NewsListRequest request = new NewsListRequest(); //假定这里是网络调用模块请求新闻列表的Request对象,细节不表  
  25.         request.setData(currentPage);  
  26.         request.request(new RequestCallback() {  
  27.             @Override  
  28.             void onSuccess(JSONObject response) {  
  29.                 totalPage = response.optInt("total_page");  
  30.                 currentPage = response.optInt("current_page");  
  31.                 //将网络数据存储到manager.......   
  32.                 saveToDataBase()  
  33.                 if(mActionCallbackerListener != null) {  
  34.                     mActionCallbackListener.onSuccess(mNewsList);  
  35.                 }  
  36.             }  
  37.             @Override  
  38.             void onFailed(Exception e, String message) {  
  39.                 if(mActionCallbackerListener != null) {  
  40.                     mActionCallbackListener.onFailed(e, message);  
  41.                 }  
  42.             }  
  43.         });  
  44.     }  
  45.   
  46.     private void getCacheFromDatabase() {  
  47.         //将缓存数据从数据库中取出  
  48.     }  
  49.   
  50.     private void saveToDatabase() {  
  51.         //缓存新闻数据至数据库  
  52.     }  
  53.   
  54.     private void clearCache() {  
  55.         //清除数据库中的缓存  
  56.     }  
  57.     //......  
  58. }  
//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 pageLoadNewsList(boolean isRefresh, final ActionCallbackListener<List<News>> mActionCallbackerListener) {
		if(isRefresh) {
			clearCache();
			mNewsList.clear();
			currentPage = 1;
		}
		NewsListRequest request = new NewsListRequest(); //假定这里是网络调用模块请求新闻列表的Request对象,细节不表
		request.setData(currentPage);
		request.request(new RequestCallback() {
			@Override
			void onSuccess(JSONObject response) {
				totalPage = response.optInt("total_page");
				currentPage = response.optInt("current_page");
				//将网络数据存储到manager....... 
				saveToDataBase()
				if(mActionCallbackerListener != null) {
					mActionCallbackListener.onSuccess(mNewsList);
				}
			}
			@Override
			void onFailed(Exception e, String message) {
				if(mActionCallbackerListener != null) {
					mActionCallbackListener.onFailed(e, message);
				}
			}
		});
	}

	private void getCacheFromDatabase() {
		//将缓存数据从数据库中取出
	}

	private void saveToDatabase() {
		//缓存新闻数据至数据库
	}

	private void clearCache() {
		//清除数据库中的缓存
	}
	//......
}
可以看到通过这样的封装,UI层根本不需要管理分页加载的逻辑,只需要调用NewsListManager的pageLoadNewsList()方法,告诉Manager是否需要刷新即可,与UI的处理逻辑(下拉刷新,分页加载)一致,这样就极大的简化了Activity和Fragment的工作。同理,这样的逻辑也可以应用于应用使用的用户数据,通过isRefresh去判断是否需要从服务端重新拉取,因为大部分应用修改用户数据的入口就那么几个,大部分情况下是不需要每次请求用户数据都用request从网络获取的,Manager实现的缓存机制就可以大幅减少不必要的接口调用,但是UI层请求数据的方法并没有任何改变。
通过Manager的封装,可以将整个应用分为三层:UI,Managers,NetModules。注意所谓分层并不是随便分几个package而已,而是有严格的职责和权限划分的,即每层都各有其职,每一层都向上层提供接口,封闭细节。比如UI层向Manager发起数据请求,并不需要关心Manager使用的是缓存还是网络请求,也不需要关心网络请求如何封装的报文参数。只需要根据Manager提供的接口参数发起请求,即可获得数据。这种架构大概是这个样子的:



由于绝大部分代码都从Activity和Fragment中剥离,现在Activity/Fragment只负责UI控制与数据获取,其它的绝大部分代码都可以做到UI无关,如果在开发中尽力确保这一点,那么在接口设计合理的基础之上,现有的Managers,网络模块以及其它一些工具类完全可以构成一个AppSDK,为手机,平板应用提供支持,项目无关的工具类和通用控件,则可以划归到公共开发资源库模块,大幅减少重复工作量。

不得不说,不需要优秀,哪怕一个靠谱的架构设计,都可以极大提升代码质量,降低工作量。这种改良版的应用架构目前在我的几个项目上都工作良好,极大减少了重复代码,不过最重要的一点是,即使在赶工阶段,已经基本什么都顾不上的状态下,依旧保持了应用整体的架构和规模没有失控,因为没有比利用框架提供的方式更快的写法。。。

总是有人说过早优化乃万恶之源,东西先出来再说,但我个人觉得无论是敏捷开发还是快速出东西,都是有门槛的,如果达不到这个门槛,为了速度而牺牲质量导致项目架构失控,代码混乱,结果就只有重构时更为辛苦,或者给别人留坑。效率为王这种概念,是针对熟练工说的,在没有经验的时候,多花点时间保证代码质量还是十分重要的。