Android应用架构

最近整理Android架构的一些东西,想到了此文,虽然是两年前的一篇文章了,却依然很有参考价值,对文中的架构演进过程深有同感,现在也有相当一部分App采用的是类似架构。

——by 译者

Android应用架构

从传统的Activity+AsyncTask到RxJava驱动的MVP架构。

这里写图片描述

一个软件代码的不同部分应该是相互独立的,但可以在一起完成工作,就像一个运转良好的机器 。

Android开发生态发展非常快,每周都会有一些新的工具和库出现或更新、新的博客和文章发表。当你休假一个月归来时可能已经有了一个新版本的Support Library或Play Services。

我曾经和ribot团队一起做过3年的Android App开发,在那段时间里,Android应用开发技术和架构不断改进,这篇文章就来分享在这个过程中我们所学到的、所犯过的错误以及架构演进背后的原因。

初始架构

回顾2012年,那是我们的代码遵循最基本的结构,没有使用任何网络框架,而是使用AsyncTask,下面这幅图大概描述了当时的架构是怎样的。

这里写图片描述

当时代码简单地被分为两层:

  1. 数据层(Data Layer)——负责通过REST API来获取数据并持久化
  2. 视图层(View Layer)——负责处理和显示数据到UI上

其中APIProvider提供了一些方法,允许ActivityFragment方便地跟REST API交互。这些方法通过使用URLConnectionAsyncTask来实现网络异步请求并通过回调将结果返回给视图层。

类似地,CacheProvider也提供了一系列方法从SharedPreferencesSQLite数据库中读取和保存数据,依然使用回调将结果传回视图层。

存在的问题

这种架构最主要的问题是视图层承担的责任太多。想象一个简单的场景:一个Activity页面需要从服务端请求一个blog post列表,并将数据缓存到SQLite数据库,最终将它们展示到一个ListView上。对于这样一个需求,这个Activity将会做以下事情:

  1. 调用APIProvider的一个方法,如loadPosts(callback)
  2. 等待APIProvider成功回调,然后调用CacheProvidersavePosts(callback)
  3. 等待CacheProvider的成功回调,然后将数据展示到ListView上
  4. 分别处理APIProviderCacheProvider中的两个潜在的回调错误

这只是一个简单的例子,实际应用场景中,视图可能无法直接使用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);
                    }
                });
}

它的工作流程是这样的:

  1. 调用Retrofit service的方法从REST API来请求blog posts数据
  2. 使用DatabaseHelper将posts数据存到本地数据库以作缓存之用
  3. 从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)

在过去的一年里,几种架构模式如MVPMVVM在Android社区变得流行起来。经过在一些示例项目上对这些模式的探索之后,我们发现MVP模式可以给我们现有架构带来有意义的改进,因为我们当前的架构分为两层(视图层和数据层),集成MVP就变得非常自然,于是我们简单地添加了一个Presenter层并把部分View中的代码移到了Presenter中。

基于MVP的架构如下所示:

这里写图片描述

原来的数据层依然保持原样,只不过为了跟MVP的名字保持一致,现在改叫做Model

Presenter层负责从Model请求数据并在数据准备好之后调用View的相关方法。Presenter订阅了从DataManager中返回的Observable,在Presenter中需要处理一些逻辑如schedulerssubscriptions,此外,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应用。

最后,希望大家喜欢这篇文章并且能够有所帮助,另外非常欢迎听到关于目前这种架构的思考和反馈。

原文地址

猜你喜欢

转载自blog.csdn.net/u014738140/article/details/76850864