Architecting Android…The evolution

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/


Architecture evolution

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

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

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

Reactive approach: RxJava

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

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

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<T>

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

到目前为止,我们的 Observable<T> 已经启动并运行,必须有人来观测它发射的数据,为了达成这个目标,我进化了 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<T>包含了一些默认的错误处理

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

让我们来列举一下通过 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路径区分:

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

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

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

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

-w458

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

Extra ball: organizing your build logic

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

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

-w214
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/从零开始/ **

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,519评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,401评论 2 45
  • 本周任务 了解JavaScript基础 掌握Bootstrap布局 常用组件的使用 时间要求 本次学习任务截止4月...
    NPU李天阅读 328评论 2 2
  • 作者|油炸橘子皮[古风] 夜行人 简介&目录上一章:[古风] 夜行人 第九章:枝节 “哒哒”的马蹄声在直隶去往滁州...
    油炸橘子皮阅读 318评论 0 0
  • 昨天晚上梦到你了,你在部队很辛苦,很累,瘦了很多,但是你很开心 。毕竟,你选择了你的方向,我只能默默地祝福你。
    寂静里安然阅读 63评论 0 0