Android应用架构变更背后的经验、失误与推论
软件代码库各个不同的部分应当彼此独立,其整体却犹如一部运转良好的机器
Android的开发生态系统发展迅速,每周都有变化,人们不停地创建新工具、更新资源库、撰写博文、发表演讲。只要享受一个月的假期,回来的时候支持库和/或Play Services都更新换代了。
笔者与 ribot团队 合作开发Android应用已有超过三年时间。在这段时间里,我们用来构建Android应用的架构与技术一直在不断进化。在本文中,我们将具体阐述这些架构变更背后的经验、失误还有推论。
过去
早在2012年,我们的代码库总是采用基础架构,并未使用任何网络库,还是用老一套的AsyncTasks。下面的图表粗略地演示了这个架构。
初始架构
代码共分两层:控制从REST API检索/保存数据的数据层(data layer),还有负责在UI上控制与展示数据的视图层(view layer)。
APIProvider提供方法,让Activities和Fragments能够很容易地与REST API交互。运用URLConnection和AsyncTasks来执行单独线程中的网络调用,并通过回调向Activities返回结果。
类似地,CacheProvider包含了从SharedPreferences或SQLite数据库检索存储数据的方式,通过回调将结果返回给Activities。
问题
这个办法的主要问题在于,视图层责任过大。试想一个简单的通用场景:应用程序在加载文章列表时,将其缓存到SQLite数据库中,并最终展示在ListView中。具体执行如下:
- 调用APIProvider中的loadPosts(回调)方法;
- 等待APIProvider成功回调,然后调用CacheProvider中的savePosts(回调);
- 等待CacheProvider成功回调,然后在ListView中显示文章;
- 分别处理APIProvider和CacheProvider的回调错误。
这是个简单的例子。在真实案例场景中,REST API可能不会按照浏览所需的那样返回数据,因此Activity会设法在展示数据之前对其进行转换或过滤。另一个常见案例:在使用 loadPosts() 方法获取需要从别处拿到的参数时,比如由Play Services SDK提供的电子邮件地址,很有可能SDK会通过回调异步返回邮件,也就是说我们现在有三层嵌套回调(nested callbacks)。如果复杂性继续增加,这个方法会导致所谓的回调地狱(callback hell)。
总结:
- Activities和Fragments逐渐过大而难以维护;
- 嵌套回调太多,导致代码丑陋不堪,难以理解与修改,也不好增加新功能;
- 单元测试也颇有难度,即便勉强进行,由于Activities或Fragments中包含有大量逻辑,相关工作也会相当费劲。
由RxJava驱动的新架构
差不多在两年时间中,我们都在采用前面描述的那种架构。在那段时间里,我们做了一些修正,但是解决问题时收效甚微。例如,我们增加了一些helper类,以减少Activities和Fragments中的代码,并开始在APIProvider中使用 Volley 。尽管如此,在应用代码测试时还是面临测试友好性问题与回调地狱频繁出现的问题。
直到2014年我们发现了 RxJava ,在尝试了几个样例项目后,我们发现这可能是解决嵌套回调问题的终极解决办法。如果对响应式编程不熟悉的话,可以参考 这篇简介 。简单来讲,RxJava允许用户通过异步流管理数据,并提供很多可用在事件流中的 operator ,方便用户修改、筛选或合并数据。
考虑到前些年遭受的痛苦,我们开始考虑新应用的架构是什么样的,然后得出了这个。
与头一个方法类似,这个架构也可以分为两层,分别是数据层与视图层。数据层包含DataManager,还有一系列helper。视图层由诸如Fragments、Activities、ViewGroups等Android框架组件构成。
Helper类(图表第三列)包含具体的职责,同时执行方式也很简洁。例如大多项目包含访问REST API的helper,从数据库读取数据的helper或者与第三方SDK交互的helper。不同的应用程序包含不同数量的helper,不过最常见的helper有:
- PreferencesHelper:在SharedPreferences中读取与保存数据。
- DatabaseHelper:处理SQLite数据库的访问。
- Retrofit 服务:从REST API执行调用。我们使用Retrofit来代替Volley,因为它提供了对RxJava的支持,也更好用。
大多数helper类中的公共方法会返回RxJava Observables。
DataManager是这个架构的核心,它广泛运用了RxJava operator来合并、筛选与转换从helper类中获得的数据。DataManager的目标是通过提供准备显示的数据,来减少Activities 和Fragments的工作量,而且这些数据一般无需任何转换。
下面的代码就是DataManager方法的实例。
- 调用Retrofit服务来加载从REST API获取的文章列表。
- 用DatabaseHelper在本地数据库中保存文章,做缓存使用。
- 按照视图层的需求,筛选出今天撰写的文章。
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); } }); }
像Activities或Fragments之类的视图层组件会简单调用这个方法,并订阅返回的Observable。一旦订阅完成,Observable所发出的不同文章就能直接加入到Adapter中,以便在RecyclerView或类似组件中显示。
这个架构的最后一个元素是Eventbus(事件总线),它允许我们将数据层的事件进行广播,因此视图层的多个组件能够订阅这些事件。例 如,DataManager中的signOut()方法可以在Observable完成时发布一个事件,让多个订阅这个事件的Activities修改 UI,显示为登出状态。
为什么这个方法更好?
- RxJava Observables和operators使得嵌套回调不再有必要。
- DataManager接管了之前视图层的部分职责,从此Activities和Fragments更为轻量。
- 将代码从Activities和Fragments中转移到DataManager和helpers中,意味着单元测试写起来更简单。
- 明确的职责分离,加上使用DataManager作为唯一与数据层的交互点,这些做法让这个架构测试时更为友好。Helper类或DataManager很容易模拟。
还有什么问题呢?
- 对于非常复杂的大型项目来说,DataManager可能会过于庞大而难以维护。
- 尽管Activities与Fragments之类的视图层组件逐渐更为轻量级,仍然需要处理相当数量的逻辑,比如管理RxJava订阅、分析错误等。
集成模型视图显示
在过去的一年中,像MVP、MVVM这样的一些架构模型在Android社区受到了热捧。在 样例项目 与 文章 中研究过这些模型之后,我们发现MVP能够对我们目前的方法带来很有价值的改进。由于我们目前的架构分为两层(视图与数据层),加上MVP也很自然。我们 只需增加一个新的展示层(a new layer of presenters),将一部分代码从视图层移过去就可以了。
基于MVP的架构
数据层保持不变,不过现在改名为模型层(Model),以便名符其实。
展示层控制加载来自模型层的数据,并在结果准备好之后调用视图层的正确方法来显示。它订阅DataManager返回的Observables,因此必须处理类似 调度 与 订阅 之类的工作。此外,它可以分析错误代码,或者在需要时在数据流中应用额外操作。例如,如果我们需要筛选一些数据,而这个筛选无法在其他地方复用,那么用展示层来实现会比在DataManager实现要更好。
下面是在展示层中公共方法的案例。这部分代码订阅了从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是这个展示层正在assist的视图层组件。一般MVP视图是Activity、Fragment或ViewGroup实例。
就像之前的架构那样,视图层包含像ViewGroups、Fragments或Activities这样的标准框架组件。这些组件的主要区别在于 没有直接订阅Observables,而是执行MVP视图,提供一系列类似showError() 或showProgressIndicator()之类的简明方法。视图组件还控制处理类似点击事件之类的与用户交互,并通过调用展示层的正确方法来执 行。例如,如果我们有一个加载文章列表的按钮,Activity就会从onClick监听那里调用 presenter.loadTodayPosts()。
想要查看基于MVP的完整架构,请查看 Android Boilerplate project on GitHub 或者 ribot’s architecture guidelines 。
为什么这个方法更好?
- Activities和Fragments都很轻量。只需负责建立/更新UI,处理用户事件。因此更容易维护。
- 我们现在能够通过模拟视图层,从展示层书写简单的单元测试了。之前这些代码是视图层的一部分,没办法进行单元测试。而且整体架构对测试更加友好。
- 如果DataManager过于庞大,我们可以通过将一些代码挪到presenter中缓解这个问题。
还有什么问题?
- 在代码库变得非常庞大与复杂时,单一的DataManager仍是个问题。我们尚未触及到真实问题点,不过迟早会碰到。
需要注意的是,这个架构并不完美。事实上,认为它是唯一而且完美的架构,能够一劳永逸的解决问题这样的想法太过天真。Android的生态系统会继续保持高速发展,我们必须持续探索、阅读、实验,才能找到构建优秀Android应用的更佳途径。
(翻译/孙薇 审校/唐小引)
文章来源:Android Application Architecture