转载

Architecting Android…The evolution

更多 Android 博文请关注我的博客 http://xuyushi.github.io

Architecting Android…The evolution

本文为翻译,在原文的基础上略有改动

http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/

在开始之前,你最好阅读过这篇文章 ( http://xuyushi.github.io/2016/07/19/Android%20clean%20architecting/ )

Architecting Android…The evolution

Architecture evolution

进化是一个循序渐进的过程,其中的一部分变的不同通常是变得更复杂更好

可以这么说,软件的发展很大程度上是软件architecture的发展。事实上,一个好的软件设计能帮助我们成长,同时能 在不需要重写的情况下,帮助我们更好的扩展

在这篇文章中,我将指出我认为重要点和关键点,让你对 Android 的代码结构更清晰,请记住这张图片,let’s stared.

Architecting Android…The evolution

Reactive approach: RxJava

这里我不再过多说 Rxjava 的好处( 你可以看看这个 ),现在有很多关于 rxjava 的 教程 ,我会指出它在 android 开发中的亮点,已经它是如何帮助我进化 clean架构的。

首先,我选择了响应式编程(也就是 rxjva,译者注) 来使得use cases(也被称为 interactors)返回 Observables ,也就是说所有的层都会成一条链式 返回 Observables

public abstract class UseCase {

private final ThreadExecutor threadExecutor;
private final PostExecutionThread postExecutionThread;

private Subscription subscription = Subscriptions.empty();

protected UseCase(ThreadExecutor threadExecutor,
PostExecutionThread postExecutionThread)
{

this.threadExecutor = threadExecutor;
this.postExecutionThread = postExecutionThread;
}

protected abstract Observable buildUseCaseObservable();

public void execute(Subscriber UseCaseSubscriber) {
this.subscription = this.buildUseCaseObservable()
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler())
.subscribe(UseCaseSubscriber);
}

public void unsubscribe() {
if (!subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
}

可以看到,所有的 cases 都继承自这个抽象类,并且实现了其中的抽象方法 buildUseCaseObservable() ,这个方法干着实际的业务活最后返回 Observable

有一点需要强调一下,我们需要确保 execute() 方法工作在独立的线程中。 这样能最小程度的阻塞 Android 的 主线程,

到目前为止,我们的 Observable 已经启动并运行,必须有人来观测它发射的数据 ,为了达成这个目标,我进化了 presenters(MVP 的一部分),使得其包含了 Subscribers ,使其来响应Observable 发射的数据

subscriber 长这样

private final class UserListSubscriber extends DefaultSubscriber<List<User>> {

@Override public void onCompleted() {
UserListPresenter.this.hideViewLoading();
}

@Override public void onError(Throwable e) {
UserListPresenter.this.hideViewLoading();
UserListPresenter.this.showErrorMessage(new DefaultErrorBundle((Exception) e));
UserListPresenter.this.showViewRetry();
}

@Override public void onNext(List<User> users) {
UserListPresenter.this.showUsersCollectionInView(users);
}
}

每个 subscriber 都是presenter中的一个内部类,并且继承自 DefaultSubscriber<T> 。DefaultSubscriber 包含了一些默认的错误处理

当了解所有的细节之后,可以通过下面这张图了解整体的思路:

Architecting Android…The evolution

让我们来列举一下通过 Rxjava 我们能获得的好处

  • ObservablesSubscribers 解耦 。使得维护和测试更方便
  • 简化了异步的任务 。java切换线程的操作还是比较复杂的。而且在 Android 中,我们需要在非主线程处理事务,在主线程更新UI,很容易出门”callbackhell”,使得代码难以阅读
  • 数据的传输和组织 。我们在不影响客户端的情况下,可以轻易合并多个 Observables<T> 。这使得我们的解决方案十分灵活
  • 错误处理 。所有的错误都可以在 subscribe 中处理。

在我看来,还是有缺点的。就是 对于不了解这些概念的开发者需要一定的学习曲线 。但是这是非常值得的。

Dependency Injection: Dagger 2

我不会过多的介绍依赖注入,因为在 这篇文章 已经介绍过了( http://xuyushi.github.io/2016/07/16/Android%20Application%20从零开始%202%20——DI/ ),这篇文章我强烈建议你读一读。我一下就简单的介绍一下

值得一提的是,通过引入像 dagger2 这样的依赖注入工具我们可以:

  • 代码的复用 ,因为依赖关系可以注入和外部配置。
  • 当注入对象时,我们可以改变对象的实现即可,不需要再调用的代码处做修改。因为 对象的构造和使用已经分离解耦
  • 依赖可注入一个组件, 它可能注入这些依赖它使测试更容易的mock实现

Lambda expressions: Retrolambda

在我们的代码中使用 java8 的 lambdas ,相信没人会抱怨。他简化了我们的代码,比如

private final Action1<UserEntity> saveToCacheAction =
userEntity -> {
if (userEntity != null) {
CloudUserDataStore.this.userCache.put(userEntity);
}
};

不过,我在这里百感交集,我来解释下原因。事实证明,在@SoundCloud我们有大约Retrolambda的讨论,主要是是否要使用它,结果是:

  1. 优点:
    • lambda表达式和方法的引用。
    • Try with resources.
    • Dev karma.(???)
  2. 缺点:
    • Accidental use of Java 8 APIs.
    • 3rd part lib, quite intrusive.
    • 3rd part gradle plugin to make it work with Android.

最后的决定取决于是否能为我们解决问题:你的代码看起来更好,更具可读性,但这些都是可有可无的。 因为现在大部分的牛逼的 IDEs 都包含了这些功能。自动了折叠了这些代码

Testing approach

在测试中,在这个例子的第一个版本没有关系大变动:

  • Presentation Layer: 使用 android instrumentatioespresso 做集成 和功能测试
  • Domain Layer: 使用 JUnitmockito 做单元测试
  • Data Layer: 使用 Robolectric (因为这层有 Android 的依赖)和 junit、mockito做集成和单元测试。

Package organization

我认为代码/包的组织良好的架构的关键因素之一:封装结构是浏览源代码时由程序员遇到的第一件事情。一切都从它流。一切都依赖于它。

我们可以把你的应用程序划分成包2路径区分:

  • 按层分包,每包中的内容通常不是密切相关,这导致低内聚 和 低模块化,包与包之间的高耦合。这将导致很多问题。例如,修改会发生在不同包的文件中,删除一个功能也会涉及到很多文件
  • 按功能分包,试图将功能相关的文件放在同一封装中,这导致在高凝聚力和高度模块化,包和包带之间的最小耦合。每个文件紧密的放在一起,而不是分散在项目的个个角落

我的建议是按照功能分包,这有以下几个优点

  • 模块化程度高
  • 更简单的代码导航
  • 范围最小

    当你在团队合作时,代码所有权会更容易组织,更模块化,这是一个不断发展的组织的一场胜利,许多开发人员在同一代码库工作。

    Architecting Android…The evolution

可以看到,我的项目结构看起来就像按层分包一样,我可能已经知道我错在哪里了。但在这种情况下我会原谅自己,因为这个例子的主旨是学习,是介绍 clean架构方法的主要概念。照我说的做,不要像我一样:) (貌似作者对其项目的分包不是很满意,不过我到觉得没啥不好,挺清晰的)

Extra ball: organizing your build logic

我们都知道,万丈高楼平地起,房子的地基很重要。软件开发也一样。构建系统(及其组织)是一个软件体系结构的一个重要的一步

在 Android 中 我们使用 gradle 来构建我们的项目。gradle 是一个与平台无关很牛逼的一个构建工具。这里说一下使用 gradle构建 应用时的一些技巧。

Architecting Android…The evolution

def ciServer = 'TRAVIS'
def executingOnCI = "true".equals(System.getenv(ciServer))

// Since for CI we always do full clean builds, we don't want to pre-dex
// See http://tools.android.com/tech-docs/new-build-system/tips
subprojects {
project.plugins.whenPluginAdded { plugin ->
if ('com.android.build.gradle.AppPlugin'.equals(plugin.class.name) ||
'com.android.build.gradle.LibraryPlugin'.equals(plugin.class.name)) {
project.android.dexOptions.preDexLibraries = !executingOnCI
}
}
}
apply from: 'buildsystem/ci.gradle'
apply from: 'buildsystem/dependencies.gradle'

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}

allprojects {
ext {
...
}
}
...

这样你就可以使用 apply from: ‘buildsystem/ci.gradle 来设置你的 gradle 文件了。 不要把所有的配置都写在一个 build.gradle 文件中,不然你的项目会变得难以维护

创建依赖

...

ext {
//Libraries
daggerVersion = '2.0'
butterKnifeVersion = '7.0.1'
recyclerViewVersion = '21.0.3'
rxJavaVersion = '1.0.12'

//Testing
robolectricVersion = '3.0'
jUnitVersion = '4.12'
assertJVersion = '1.7.1'
mockitoVersion = '1.9.5'
dexmakerVersion = '1.0'
espressoVersion = '2.0'
testingSupportLibVersion = '0.1'

...

domainDependencies = [
daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}",
dagger: "com.google.dagger:dagger:${daggerVersion}",
javaxAnnotation: "org.glassfish:javax.annotation:${javaxAnnotationVersion}",
rxJava: "io.reactivex:rxjava:${rxJavaVersion}",
]

domainTestDependencies = [
junit: "junit:junit:${jUnitVersion}",
mockito: "org.mockito:mockito-core:${mockitoVersion}",
]

...

dataTestDependencies = [
junit: "junit:junit:${jUnitVersion}",
assertj: "org.assertj:assertj-core:${assertJVersion}",
mockito: "org.mockito:mockito-core:${mockitoVersion}",
robolectric: "org.robolectric:robolectric:${robolectricVersion}",
]
}
apply plugin: 'java'

sourceCompatibility = 1.7
targetCompatibility = 1.7

...

dependencies {
def domainDependencies = rootProject.ext.domainDependencies
def domainTestDependencies = rootProject.ext.domainTestDependencies

provided domainDependencies.daggerCompiler
provided domainDependencies.javaxAnnotation

compile domainDependencies.dagger
compile domainDependencies.rxJava

testCompile domainTestDependencies.junit
testCompile domainTestDependencies.mockito
}

如果你希望在其他 module 中也使用同一的 version,或者使用同样的依赖,这样做很有效。另一个好处就是 你可以在一个地方控制所有的依赖

Wrapping up

记住,没有银弹,但是每个好的软件架构会帮我们的代码结构保持整洁和健康,方便扩展便于维护

我现在根据这个架构为模板做一个开源 APP,完成以后会开源,详见请见 http://xuyushi.github.io/tags/从零开始/

原文  http://xuyushi.github.io/2016/07/20/Architecting Android…The evolution/
正文到此结束
Loading...