原文链接:https://medium.com/@laanayabdrzak/architecting-modern-mobile-applications-bf896120f0c2#.n1m5i520c
在完成了一些项目之后,我根据个人经验,总结了一下如何正确地设计一个Android应用架构。
首先要分享的就是Bob大叔的一篇文章
好的架构有如下特点:
- 独立于UI
- 独立于任何框架
- 独立于任何第三方服务
- 独立于数据库
- 易于测试
为什么要把精力花在架构上
在开始这篇文章之前,我要讲一讲架构的重要性以及为什么要花费时间和资源去搭建一个好的架构。有的开发团队不设计架构直接开工,这样做看似省去不少麻烦,但一般结果都不佳,所以开发前一定要先设计好架构。
使用MVP
近些年,有许多Android架构在开发社区中颇受青睐,比如MVP或MVVM,讨论它们的人越来越多。
为什么要使用MVP?
在Android开发中层有一个大问题,就是Activity和接口与数据获取类耦合太紧,导致View层的任务太重,几乎无法维护或扩展。理想情况下MVP要将不同View中的逻辑分离出来。MVP将View从数据中分离出来,并将整个应用分为至少三层,而这三层又能分别测试。
- Model层包含要展示在View(UI)中的数据
- View层包含负责展示数据并将用户事件传递给Presenter的接口
- Presenter是Model和View的中间人,它持有二者的引用。
使用ReactiveX: RxJava/RxAndroid
当一个安卓应用中有很多的网络操作、用户交互与动画代码时,项目中可能会有大量的回调方法,也就出现了回调地狱(Callback Hell)。ReactiveX提供了另一种处理异步任务和事件的解决方案。
RxJava是一个响应式扩展(Reactive Extensions)的JVM实现。由NetFlix开发,当下正火。
RxJava现在正是Android开发社区的焦点。虽然上手可能会有些困难,但一旦真正使用上RxJava,你就会喜欢上RxJava,因为它能非常cool地避免回调地狱。
使用依赖注入框架: Dagger2
在Clean架构中,代码被分层成洋葱状,并遵循一个基本原则:内层不知道有关外层的任何东西。也就是说依赖是由外向内的。Dagger2有如下特点:
- 因为依赖可以在外部被注入和配置,所以组件可以重用。
- 依赖的注入和配置独立于组件之外,注入的对象在一个独立、不耦合的地方初始化,这样在改变注入对象时,我们只需要修改对象的实现方法,而不用大改代码库。
- 依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单。
使用Dagger2会带来至少几点优势:
- 简化对共享实例的访问,就像ButterKnife一样
- 即使对于复杂的依赖,配置也很简单
- 简化单元测试
- 给实例设置区间,让你轻松管理不同生命周期(甚至是Application生命周期)的实例。
使用Lambda表达: Retrolambda
Retrolambda是用来在Android与非jdk8平台上使用Lambda表达式的Java库。它让你的代码更紧凑且可读性更强,尤其适合配合RxJava等函数式代码使用。
正确地打包JAVA代码: 依据功能而不是层
在编写一个应用时会问的一个问题就是把代码分在哪几个包里?对于传统的应用,一般有两种打包方式:
依据功能打包
在这种打包方式中,包名对应着任务,或说功能。每个包中一般只包括负责对应功能的类,比如如下结构:
<code class="hljs ruby has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">abderrazak.com.recycleviewcardview
├─ data
│ ├─ local
│ ├─ model
│ └─ remote
├─ injection
│ ├─ component
│ └─ <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">module</span></span>
├─ ui
│ ├─ main
│ ├─ detail
│ └─ etc..
├─ util
└─ views
├─ adapters
└─ widgets</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li></ul>
这种打包方式的优势:
- 可以一下看出APP是干什么的。
- 代码模块化程度高。
- 更方便找文件。
- 抽象程度更高。
- 将不同功能与不同层有效分开。
- 结构易读且易维护。
- 文件间粘合度高。
- 不容易错误修改类或文件。
- 易于添加或移除应用功能。
- 模块易于复用。
依据层打包
在这种打包方式中,最上层的包对应着应用的不同层,而不是功能,比如如下结构:
<code class="hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">abderrazak<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.com</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">.recycleviewcardview</span>
├
├─ model
├─ activities
├─ services
├─ fragments
├─ util
└─ etc..</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul>
在这种项目结构中,每个功能的实现被分散在不同的目录中。每个目录中的文件之间没有什么关系,这就导致项目包粘合度低并且模块化差,且不同包之间耦合度高。结果就是,当修改功能时,会在不同的包中修改各种文件,尤其几乎无法删除某个功能。
项目构建:使用Gradle
开发Android,就用Gradle,这本来就是安卓开发用的官方构建工具,下面罗列一下使用这个自动化构建工具的一部分好处:
- 构建APP的不同风格版本。
- 下载并管理依赖库。
- 自定义keystore 。
- 适应Android项目结构。
测试:Espresso/JUnit/Mockito/Robolectric
在开发的过程中就应该有测试的环节。测试可以让你确认程序是否正确、功能是否运行、整个应用是否可用等。
- Presentation层:使用Espresso2和Instrumentation来测试UI。
- Domain层:因为是纯Java层,所以使用JUnit和Mockito进行测试。
- Data层:使用Robolectric3、JUnit和Mockito测试。曾经这个层的测试代码需要单写一个模块,因为那时IDE没有内置单元测试,所以搭建一个类似于Robolectric的框架并不容易。
总结
幸运的是,应用架构是当下焦点,有太多的文章和博客在探讨不同的Android应用架构,从老牌的MVC/MVP/MVVM到一些新奇的方式比如Square使用的Mortar和Flow。
希望这篇文章可以帮助你更好的架构未来的Android应用。