Android Application Architecture
我们从标准活动和AsyncTasks到由RxJava支持的基于MVP的现代架构的旅程。
Android开发生态系统变得非常快。每周都会创建新工具,更新Lib,写博客文章和发言。如果你去度假一个月,当你回来的时候会有一个新版本的支持库和/或Play服务。
我已经使用ribot团队制作Android应用程序三年多了。在此期间,我们用于构建Android应用程序的架构和技术不断发展。本文将通过解释我们的学习,错误和这些架构变化背后的推理,带您走过这段旅程。
旧时代
回到2012年,我们的代码库用于遵循基本结构。我们没有使用任何网络库,AsyncTasks仍然是我们的朋友。下图显示了大致的架构。
初始架构
代码分为两层:负责从REST API和持久性数据存储检索/保存数据的数据层;和视图层,其职责是在UI上处理和显示数据。
APIProvider提供了使活动和片段能够轻松地与REST API交互的方法。这些方法使用URLConnection和AsyncTasks在单独的线程中执行网络调用,并通过回调将结果返回到活动。
以类似的方式,CacheProvider包含从SharedPreferences或SQLite数据库检索和存储数据的方法。它还使用回调将结果传递回活动。
问题
这种方法的主要问题是View层有太多的责任。想象一个简单的常见场景,其中应用程序必须加载博客帖子列表,将它们缓存在SQLite数据库中,并最终显示在ListView上。活动必须做到以下几点:
在APIProvider中调用loadPosts(回调)方法
等待APIProvider成功回调,然后在CacheProvider中调用savePosts(callback)。
等待CacheProvider成功回调,然后在ListView上显示帖子。
单独处理来自APIProvider和CacheProvider的两个潜在错误回调。
这是一个非常简单的例子。在实际情况下,REST API可能不会返回视图需要的数据。因此,活动必须以某种方式在显示数据之前转换或过滤数据。另一个常见的情况是loadPosts()方法接收需要从其他地方获取的参数,例如Play Services SDK提供的电子邮件地址。很可能SDK将使用回调异步返回电子邮件,这意味着我们现在有三个层次的嵌套回调。如果我们继续增加复杂性,这种方法将导致所谓的回调地狱。
综上所述:
活动和碎片变得非常大,难以维护
太多的嵌套回调意味着代码是丑陋的,很难理解这么痛苦地做出更改或添加新的功能。
单元测试变得具有挑战性,如果不是不可能的,因为很多逻辑生活在活动或片段内,是艰苦的单元测试。
一个由RxJava驱动的新架构
我们按照以前的方法约两年。在此期间,我们做了几个改进,轻微缓解了上述问题。例如,我们添加了几个帮助类来减少活动和片段中的代码,我们开始在APIProvider中使用Volley。尽管有这些变化,我们的应用程序代码还不是测试友好的,回调地狱问题仍然发生得太频繁。
直到2014年,我们才开始阅读关于RxJava。在一些示例项目上尝试之后,我们意识到这最终可以是嵌套回调问题的解决方案。如果你不熟悉反应式编程,你可以阅读这个介绍。简而言之,RxJava允许您通过异步流管理数据,并为您提供了许多运算符,您可以应用于流,以便变换,过滤或组合数据。
考虑到我们在过去几年中经历的痛苦,我们开始考虑一个新应用程序的架构如何看起来。所以我们想出了这个。
RxJava驱动架构
与第一种方法类似,该架构可以分为数据层和视图层。数据层包含DataManager和一组帮助程序。视图层由Android框架组件(如Fragments,Activities,ViewGroups等)构成。
辅助类(图中第三列)具有非常具体的职责,并以简明的方式实现它们。例如,大多数项目都有帮助访问REST API,从数据库读取数据或与第三方SDK交互。不同的应用程序将有不同数量的助手,但最常见的是:
PreferencesHelper:在SharedPreferences中读取和保存数据。
DatabaseHelper:处理访问SQLite数据库。
改进服务:执行对REST API的调用。我们开始使用Retrofit而不是Volley,因为它为RxJava提供了支持。它也更好使用。
辅助类中的大多数公共方法将返回RxJava Observable。
DataManager是架构的大脑。它广泛地使用RxJava运算符组合,过滤和转换从辅助类检索的数据。 DataManager的目的是通过提供准备显示的数据来减少Activity和Fragments必须做的工作量,并且通常不需要任何转换。
下面的代码显示了DataManager方法的外观。此示例方法的工作原理如下:
调用Retrofit服务以从REST API加载博客文章列表
使用DatabaseHelper将帖子保存在本地数据库中以进行缓存。
过滤今天写的博客文章,因为那些是唯一的视图层想要显示的。
public Observable loadTodayPosts() {
return mRetrofitService.loadPosts()
.concatMap(new Func1, Observable>() {
@Override
public Observable call(List apiPosts) {
return mDatabaseHelper.savePosts(apiPosts);
}
})
.filter(new Func1() {
@Override
public Boolean call(Post post) {
return isToday(post.date);
}
});
}
https://gist.github.com/ivacf/b84654c3c5984c84401e/raw/134c4ccf4ffddef58ab8a5caa45d3ad30168e8af/DataManager.java
视图层中的组件(如Activities或Fragments)将简单地调用此方法并订阅返回的Observable。一旦订阅完成,Observable发出的不同的帖子可以直接添加到适配器,以便显示在RecyclerView或类似的。
这种架构的最后一个元素是事件总线。事件总线允许我们广播在数据层中发生的事件,以便视图层中的多个组件可以订阅这些事件。例如,DataManager中的signOut()方法可以在Observable完成时发布事件,以便订阅此事件的多个活动可以更改其UI以显示签出状态。
为什么这种方法更好?
RxJava Observables和运算符删除了嵌套回调的需要。
DataManager接管以前是视图图层的一部分的职责。因此,它使Activities和Fragments更轻量级。
将活动和片段中的代码移动到DataManager和助手意味着编写单元测试变得更容易。
清楚地分离职责,使DataManager成为与数据层交互的唯一点,使得这种架构对测试友好。辅助类或DataManager可以轻松地嘲笑。
我们还有什么问题?
对于大型和非常复杂的项目,DataManager可能变得过于。肿和难以维护。
虽然视图层组件(如活动和片段)变得更轻量级,但它们仍然需要处理大量关于管理RxJava订阅,分析错误等的逻辑。
在过去一年中,几个架构模式,如MVP或MVVM已经在Android社区中越来越受欢迎。在对示例项目和文章探索这些模式之后,我们发现MVP可以为我们现有的方法带来非常有价值的改进。因为我们当前的架构分为两层(视图和数据),增加MVP感觉自然。我们只需要添加一个新的演示者层,并将代码的一部分从视图移动到演示者。
基于MVP的架构
数据层保持原样,但它现在称为模型,以更加一致的模式的名称。
演示者负责从模型加载数据,并在结果准备好时调用视图中的正确方法。他们订阅由数据管理器返回的Observables。因此,他们必须处理像调度程序和订阅。此外,如果需要,他们可以分析错误代码或对数据流应用额外的操作。例如,如果我们需要过滤一些数据,并且这个相同的过滤器不可能在其他任何地方重复使用,那么在演示者而不是数据管理器中实现它可能更有意义。
下面你可以看到一个公共方法在演示者看起来像。此代码订阅了我们在上一节中定义的dataManager.loadTodayPosts()方法返回的Observable。
public void loadTodayPosts() {
mMvpView.showProgressIndicator(true);
mSubscription = mDataManager.loadTodayPosts().toList()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Subscriber
>() {
@Override
public void onCompleted() {
mMvpView.showProgressIndicator(false);
}
@Override
public void onError(Throwable e) {
mMvpView.showProgressIndicator(false);
mMvpView.showError();
}
@Override
public void onNext(List postsList) {
mMvpView.showPosts(postsList);
}
});
}
mMvpView是此演示者正在协助的视图组件。通常MVP视图是一个Activity,Fragment或ViewGroup的实例。
与以前的架构一样,视图层包含标准框架组件,如ViewGroups,Fragments或Activities。主要的区别是这些组件不直接订阅Observable。它们实现了一个MvpView接口,并提供了简单的方法列表,如showError()或showProgressIndicator()。视图组件还负责处理诸如点击事件的用户交互,并且通过调用演示者中的正确方法来相应地操作。例如,如果我们有一个加载帖子列表的按钮,我们的Activity将从onClick侦听器调用presenter.loadTodayPosts()。
如果你想看到这个基于MVP架构的一个完整的工作示例,你可以在GitHub上查看我们的Android Boilerplate项目。您还可以在ribot的架构指南中阅读更多内容。
为什么这种方法更好?
活动和片段变得非常轻量级。他们唯一的职责是设置/更新UI并处理用户事件。因此,它们变得更容易维护。
我们现在可以通过模拟视图层轻松地为演示者编写单元测试。之前,这段代码是视图层的一部分,所以我们不能单元测试它。整个架构变得非常测试友好。
如果数据管理器变得。肿,我们可以通过将一些代码移动到演示者来缓解这个问题。
我们还有什么问题?
当代码库变得非常大和复杂时,拥有单个数据管理器仍然是一个问题。我们还没有达到这是一个真正的问题,但我们知道,它可能发生。
重要的是要提到这不是完美的建筑。事实上,认为有一个独特和完美的,将永远解决你的所有问题是天真的。 Android生态系统将保持快速发展,我们必须通过探索,阅读和实验,以便我们可以找到更好的方式来继续构建优秀的Android应用程序。
我希望你喜欢这篇文章,你发现它有用。如果是这样,不要忘记点击推荐按钮。此外,我很乐意听到你对我们最新方法的想法。
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); |
|
} |
|
}); |
|
} |
|
|
public Observable loadTodayPosts() {
return mRetrofitService.loadPosts()
.concatMap(new Func1, Observable>() {
@Override
public Observable call(List apiPosts) {
return mDatabaseHelper.savePosts(apiPosts);
}
})
.filter(new Func1() {
@Override
public Boolean call(Post post) {
return isToday(post.date);
}
});
}