Android应用架构

xiaoxiao2021-02-28  127

最近整理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」,技术之外的分享:

转载请注明原文地址: https://www.6miu.com/read-96223.html

最新回复(0)