Android应用架构
Posted aspook
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android应用架构相关的知识,希望对你有一定的参考价值。
最近整理android架构的一些东西,想到了此文,虽然是两年前的一篇文章了,却依然很有参考价值,对文中的架构演进过程深有同感,现在也有相当一部分App采用的是类似架构。
——by 译者
Android应用架构
从传统的Activity+AsyncTask到RxJava驱动的MVP架构。
一个软件代码的不同部分应该是相互独立的,但可以在一起完成工作,就像一个运转良好的机器 。
Android开发生态发展非常快,每周都会有一些新的工具和库出现或更新、新的博客和文章发表。当你休假一个月归来时可能已经有了一个新版本的Support Library或Play Services。
我曾经和ribot团队一起做过3年的Android App开发,在那段时间里,Android应用开发技术和架构不断改进,这篇文章就来分享在这个过程中我们所学到的、所犯过的错误以及架构演进背后的原因。
初始架构
回顾2012年,那是我们的代码遵循最基本的结构,没有使用任何网络框架,而是使用AsyncTask,下面这幅图大概描述了当时的架构是怎样的。
当时代码简单地被分为两层:
- 数据层(Data Layer)——负责通过REST API来获取数据并持久化
- 视图层(View Layer)——负责处理和显示数据到UI上
其中APIProvider提供了一些方法,允许Activity或Fragment方便地跟REST API交互。这些方法通过使用URLConnection和AsyncTask来实现网络异步请求并通过回调将结果返回给视图层。
类似地,CacheProvider也提供了一系列方法从SharedPreferences或SQLite数据库中读取和保存数据,依然使用回调将结果传回视图层。
存在的问题
这种架构最主要的问题是视图层承担的责任太多。想象一个简单的场景:一个Activity页面需要从服务端请求一个blog post列表,并将数据缓存到SQLite数据库,最终将它们展示到一个ListView上。对于这样一个需求,这个Activity将会做以下事情:
- 调用APIProvider的一个方法,如
loadPosts(callback)
- 等待APIProvider成功回调,然后调用CacheProvider的
savePosts(callback)
- 等待CacheProvider的成功回调,然后将数据展示到ListView上
- 分别处理APIProvider和CacheProvider中的两个潜在的回调错误
这只是一个简单的例子,实际应用场景中,视图可能无法直接使用REST API返回的数据,因此视图层需要先对数据做一些转换或过滤才能显示。另一种场景可能是loadPosts()
方法需要参数,但这个参数可能要从其他地方获取,例如Play Services SDK提供了一个Email地址,一般来说SDK会通过异步回调将Email地址返回,这意味着现在有3层嵌套回调,如果逻辑变得更加负责的话,那将会导致可怕的回调地狱(callback hell)。
总结如下:
- Activity和Fragment变得越来越庞大而难以维护
- 太多嵌套的回调将使代码变得非常丑陋且难以理解,修改逻辑或添加新功能将变得异常痛苦
- 单元测试将变得困难,因为Activity和Fragment中维护了大量的逻辑,这对单元测试来说非常具有挑战性
一种RxJava驱动的新架构
上述架构方案我们大概用了两年,在那段时间,我们做了几次改进来解决上面提到的问题,效果比较一般。例如,我们添加了几个帮助类来减少Activity和Fragment中的代码;在APIProvider中尝试使用Volley。尽管我们做了这些改进,但我们的应用仍然对测试不友好,且callback hell还是经常发生。
直到2014年,我们开始了解RxJava,经过几个示例项目的尝试之后,我们意识到RxJava是解决嵌套回调问题的最终答案。如果你对响应式编程还不熟悉,可以阅读一下其说明文档。简单来说,RxJava允许使用异步流的方式来管理数据,并提供了许多操作符可以用来转换、过滤或组合数据,并将其应用到流上。
鉴于上文所描述的之前遇到的痛点,我们开始考虑这种新的App架构应该是怎么样的,所以有了下面这种RxJava驱动的架构:
跟之前的架构类似,上图的架构也可以分为数据层和视图层。其中数据层包括DataManager和一系列帮助类,视图层则由Android框架组件如Activity、Fragment等组成。
帮助类(上图中的第三列)拥有特定的职责并以一种简洁的方式实现。例如,许多项目有访问REST API的帮助类、从数据库读取数据的帮助类、跟第三方SDK交互的帮助类。不同的应用有不同数量的帮助类,但常用的是如下几个:
- PreferencesHelper——利用SharedPreferences读取和保存数据
- DatabaseHelper——处理SQLite数据库的数据访问
- Retrofit services——负责调用REST API,我们使用Retrofit来替代Volley,因为它支持RxJava且对于用户非常易用
大部分帮助类中的方法将返回RxJava的被观察对象(RxJava Observables,RxJava中的概念)。
在这个架构中,DataManager是核心所在,它广泛使用RxJava的操作符来组合、过滤、转换从帮助类中获取的数据。DataManager的目标是减少Activity和Fragment中的数据转换操作等逻辑,DataManager提供可直接供视图层展示的数据,数据到视图层后就不需要再做处理。
下面代码展示了一个DataManager方法示例:
public Observable<Post> loadTodayPosts()
return mRetrofitService.loadPosts()
.concatMap(new Func1<List<Post>, Observable<Post>>()
@Override
public Observable<Post> call(List<Post> apiPosts)
return mDatabaseHelper.savePosts(apiPosts);
)
.filter(new Func1<Post, Boolean>()
@Override
public Boolean call(Post post)
return isToday(post.date);
);
它的工作流程是这样的:
- 调用Retrofit service的方法从REST API来请求blog posts数据
- 使用DatabaseHelper将posts数据存到本地数据库以作缓存之用
- 从blog posts数据中过滤出当天的数据,因为视图层只会显示当天的数据
视图层的组件如Activity或Fragment只需要简单地调用上述方法,然后订阅(subscribe,RxJava中的概念)返回的可订阅对象(Observable)即可。一旦订阅关系建立后,Observable中触发的数据可以直接添加到Adapter中以便在RecyclerView或其他类似控件中展示。
这个架构中还用到了Event Bus,Event Bus允许我们使用广播发送数据层的事件(event),所以视图层的各个组件可以订阅这些广播事件。例如,DataManager中的**signOut()**方法在Observable完成时可以发送一个event,这样视图层所有订阅过该event的Activity在收到事件广播后可以将UI效果置为登出状态。
为什么这种架构更好
- RxJava的Observable及操作符替换掉了之前的嵌套回调
- DataManager接管了部分原属于视图层的职责,因此Activity和Fragment将变得更加轻量级
- 将代码从Activity和Fragment中移到DataManager或帮助类中,从而使编写单元测试变得容易
- 明确了职责分离,DataManager作为唯一跟数据层交互的入口,测试非常友好。帮助类及DataManager很容易做数据mock
依然存在的问题
- 对于庞大复杂的项目来说,DataManager将变得非常臃肿,难以维护
- 尽管视图层的组件如Activity和Fragment变得轻量级,它们仍然需要处理相当数量的关于RxJava订阅相关的逻辑、错误处理等
集成MVP(Model、View、Presenter)
在过去的一年里,几种架构模式如MVP、MVVM在Android社区变得流行起来。经过在一些示例项目上对这些模式的探索之后,我们发现MVP模式可以给我们现有架构带来有意义的改进,因为我们当前的架构分为两层(视图层和数据层),集成MVP就变得非常自然,于是我们简单地添加了一个Presenter层并把部分View中的代码移到了Presenter中。
基于MVP的架构如下所示:
原来的数据层依然保持原样,只不过为了跟MVP的名字保持一致,现在改叫做Model。
Presenter层负责从Model请求数据并在数据准备好之后调用View的相关方法。Presenter订阅了从DataManager中返回的Observable,在Presenter中需要处理一些逻辑如schedulers和subscriptions,此外,Presenter还将进行错误处理,如果需要的话还可以对数据流进行额外的操作。例如,如果需要对数据进行某种过滤处理,而这个过滤器又无法在其他地方重用,因此在Presenter中实现过滤器比在DataManager中实现更有意义。
下面你将会看到Presenter的一个方法的示例代码,这段代码订阅了从dataManager.loadTodayPosts()
中返回的Observable:
public void loadTodayPosts()
mMvpView.showProgressIndicator(true);
mSubscription = mDataManager.loadTodayPosts().toList()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Subscriber<List<Post>>()
@Override
public void onCompleted()
mMvpView.showProgressIndicator(false);
@Override
public void onError(Throwable e)
mMvpView.showProgressIndicator(false);
mMvpView.showError();
@Override
public void onNext(List<Post> postsList)
mMvpView.showPosts(postsList);
);
其中mMvpView是当前Presenter对应的视图组件,MVP中的View一般是Activity、Fragment或ViewGroup的实例。
跟之前的架构类似,视图层包含标准的框架组件,如Activity、Fragment或ViewGroup,主要的不同在于视图组件不再直接订阅Observable,而是实现一个MvpView接口并提供一系列简洁的方法如showError()
或showProgressIndicator()
。视图组件依然负责处理用户交互如点击事件,并调用Presenter中的对应方法。例如,一个点击加载posts列表的Button,在响应到View的onClick事件后会调用presenter.loadTodayPosts()
。
如果你想看到一个完整的基于MVP架构的示例,可以访问我们的GitHub,地址为Android Boilerplate project on GitHub,也可以通过 ribot’s architecture guidelines阅读更多内容。
为什么这种架构更好
- Activity和Fragment变得非常轻量级,它们仅有的职责是设置、更新UI以及处理用户交互,因此很容易维护
- 现在可以非常轻松地通过mock数据层来为Presenter编写单元测试。在以前,这部分代码是View层的一部分因此不方便单元测试,现在整个架构对测试也变得更加友好
- 如果DataManager变得臃肿庞大,可以将部分代码转移到Presenter中来缓解此问题
依然存在的问题
- 当代码库变得更庞大更复杂的时候,只有一个单独的DataManager将成为问题。虽然目前我们还没有触碰到这个天花板,但这将是一个迟早会发生的问题。
。。。
必须要说明的是这非是一个完美的架构,事实上,一个能够永远解决所有问题的完美架构是不存在的。Android生态系统将继续快速发展而我们也将继续探索、阅读和实践,因此我们将会持续发现更好的方法来构建杰出的Android应用。
最后,希望大家喜欢这篇文章并且能够有所帮助,另外非常欢迎听到关于目前这种架构的思考和反馈。
我的微信公众号「不混青年」,id「buhunqingnian」,技术之外的分享:
以上是关于Android应用架构的主要内容,如果未能解决你的问题,请参考以下文章