写点什么

使用 Clean Architecture 模式开发 Android 应用的详细教程

  • 2016-03-16
  • 本文字数:7495 字

    阅读完需:约 25 分钟

【编者的话】随着应用体积和代码数量的膨胀,Android 应用的架构越来越复杂,遗留代码越来越多,接手开发、协作都变得越来越困难,有些人试图用框架、规范来解决这个问题,但为什么不从一开始就从架构上着手呢?干净架构就是一种很好的层级解耦、理清依赖的架构,作者在接触干净架构后就喜欢上了它,不仅在商业上成功应用,还撰文介绍、开源样板代码,简直化身干净架构布道师了,我们一起来看作者是如何向我们安利这一架构吧。

自从我开始进行 Android 应用的开发,我一直以为这项工作可以完成的更好。在我的职业生涯中,我见到过很多错误的软件设计决定,其中有一些还是我自己的。而且,这些决定导致了 Android 应用设计复杂度的急剧膨胀。但是,从你的错误中吸取教训并不断改进以后的做法才是非常重要的。在探索了很多应用开发的方法后,我遇到了干净架构(Clean Architecture)。在将其进行改良并引入了一些类似项目的灵感后,我把这种方法应用到了 Android 开发,发现该方法完全可以用于实践,值得推荐和分享。

这篇文章的目的是提供利用 Clean 方法进行 Android 应用开发的一个指南。它已在我最近为客户开发应用过程中得到成功验证。

何为干净架构

我不准备在这里解释太多细节,因为网上已经有了不少的资料(译者注:中文的资料也有一些)。下面我会给出理解 Clean 的关键信息。

Clean 一般是指,代码以洋葱的形状依据一定的依赖规则被划分为多层:内层对于外层一无所知。这就意味着依赖只能由外向内

其具体含义可以通过下图进行很好的展示。

(此图来源于 Bob 大叔,他也是干净架构的提出者)

干净架构使得代码可以实现:

  • 框架的独立性
  • 可测试
  • UI 的独立性
  • 数据库的独立性
  • 外部代理的独立性

在后面的例子中,我会详细讲解这些特征是如何达成的。我强烈推荐 8thlight 的一篇文章( https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html )和 vimeo 上的视频( https://vimeo.com/43612849 )来详细了解 Clean 的概念。

对 Android 来说的意义

一般而言,你的应用可以有任意数量的层。但是,除非需要在所有的 Android 应用中采用企业级的业务逻辑,一般的应用至多只有三层:

  • 外层:实现层
  • 中层:接口适配层
  • 内层:业务逻辑层

实现层包含了所有框架相关的东西。框架相关的代码包含了所有不是专门用来解决目标问题的部分,例如创建 activity 和 fragment、发送目标以及网络和数据库相关的框架代码。

接口适配层的目的就是负责连接业务逻辑和框架相关的代码。

应用中最重要的就是业务逻辑层。它负责解决你的应用所真正想解决的问题。该层不包含任何框架相关的代码,因此其代码应该可以在没有模拟器的情况下独立运行。这样,测试、开发和维护业务逻辑代码就要容易很多。而这就是干净架构的主要优势。

在核心层以上的每一层,也都负责在更低层使用模型之前将它们转换为更低层的模型。内层不能使用任何属于外部的模型类的引用;但是外层可以使用和引用内层的模型——这都是因为依赖规则。这种方式会造成一定的开销,但确保了层与层之间代码的解耦合。

** 为什么模型转换是必须的呢?那是因为,业务逻辑模型未必适合直接向用户进行展示,或是需要一次展示多个业务逻辑模型的组合。因此,我建议先创建一个 ViewModel 类来充当中间人。然后,在外层使用一个转换类将业务模型转换为合适的 ViewModel。

另外一个例子如下:假如从一个外部数据库层的 ContentProvider 中得到了一个 Cursor 对象。那么,外层首先将该对象转换为内部的业务模型,然后将其发送到业务逻辑层进行处理。

在文章最后,我会添加更多的相关资源来学习。现在我们已经知道了干净架构的基本概念,文章接下来将会用一些实际的代码来进行说明。下一节,我将展示了如何使用 Clean 来设计一个示例功能。

我所作的一些准备

我已经创建了一个框架搭建完毕的样例工程。以此作为 Clean starter pack,你可以利用其中的常见的工具继续开发。该工程已发布在 Github,你可以免费下载、修改和使用。

项目地址: https://github.com/dmilicic/Android-Clean-Boilerplate

开始编写一个新用例

本节将会对使用 Clean 方法创建一个用例所需要编写的代码进行解释。在这里,一个用例只是某个应用中被专门隔离出的部分功能。该用例可能被用户直接使用(例如,用户点击),也可能不被用户直接使用。

首先,对该方法的结构和术语进行解释。这只是我自己构建应用的方式,并非一成不变,你可以根据需求自己进行相应的变化。

结构

一个 Android 应用的通用结构如下:

  • 外层包:UI、存储、网络等
  • 中间层包:Presenter、Converter
  • 内层包:Interactor、Model、Repository、Executor

外层

正如之前所提到的,该层是框架的具体细节所在。

UI——这是所有 Activity、Fragment、适配模块和其他与用户接口相关的 Android 代码存在的地方。

存储——Interactor 访问和存储数据所需要使用的接口代码。例如,它包括了 ContentProvider 或者 DBFlow 等 ORM。

网络——包括了 Retrofit 等。

中间层

负责将实现细节和业务逻辑连接起来的粘合层。

Presenter——Presenter 负责处理来自 UI 的事件(如用户点击等)和内层模块(如 Interaactor 等)的回调。

Converter——Converter 对象负责内层模型与外层模块的相互转换工作。

内层

该层包含了绝大部分高级代码。其中所有的类都是 POJO。该层中的类和对象对于 Android 应用相关的东西一无所知,因此可以被轻易移植到任何运行 JVM 的机器中。

Interactor——这就是包含实际的业务逻辑代码的类。他们在后台运行,并通过回调函数将事件报告给上层。在一些项目中,他们也被称作用例。通常情况下,项目中可能包含很多小的 Interactor 类,用来分别解决特定的问题。这符合了单一职责原则,也比较容易对类进行理解。

模型——这些就是在业务逻辑中进行处理的业务模型。

Repository——该包只包含了数据库或者其他外层实现的接口。Interactor 使用这些接口来访问和存储数据。这就是所谓的 repository 模式( https://msdn.microsoft.com/en-us/library/ff649690.aspx )。

Executor——该包涵盖了使得 Interactor 在后台运行所需要的代码。一般情况下,用户不需要修改该包。

一个简单的例子

在该例子中,用例是:“当应用启动的时候,显示存储在数据库中的欢迎消息”。接下来,文章就分析如何编写该用例所需要用到的三个包:

  • 显示
  • 存储

其中,前两个包属于外层,而最后一个属于内层(核心层)。

显示包负责将相关内容显示到屏幕上,因而它包含了整个的 MVP 栈(这就意味着该包包含了属于不同层的 UI 包和 Presenter 包)。

接下来,让我们开始讨论代码。

编写一个新的 Interactor(内层 / 核心层)

你可以从结构中的任何一层开始编写,但是我推荐首先从核心业务逻辑开始。你可以编写、测试,并在没有 activity 的情况下确保其正常工作。

因此,首先从创建 Interactor 开始。Interactor 就是用例的核心逻辑所在的地方。所有的 Interactor 都以后台线程的方式运行,不会对 UI 性能造成任何影响。假设将要创建的 Interactor 的名字为 WelcomingInteractor。

复制代码
public interface WelcomingInteractor extends Interactor {
interface Callback {
void onMessageRetrieved(String message);
void onRetrievalFailed(String error);
}
}

Callback 接口负责与主线程中的 UI 进行通信。它在 Interactor 接口内,因此并不需要将其专门命名为 WelcomingInteractorCallback 才可以与其他回调进行区别。接下来,开始实现提取消息的逻辑。设定给予欢迎消息的接口为 MessageRepository 。

复制代码
public interface MessageRepository {
String getWelcomeMessage();
}

然后,开始实现带有业务逻辑的 Interactor 接口。非常重要的是,其实现扩展了负责在后台线程运行它的 AbstractInteractor。

复制代码
public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {
...
private void notifyError() {
mMainThread.post(new Runnable() {
@Override
public void run() {
mCallback.onRetrievalFailed("Nothing to welcome you with :(");
}
});
}
private void postMessage(final String msg) {
mMainThread.post(new Runnable() {
@Override
public void run() {
mCallback.onMessageRetrieved(msg);
}
});
}
@Override
public void run() {
// retrieve the message
final String message = mMessageRepository.getWelcomeMessage();
// check if we have failed to retrieve our message
if (message == null || message.length() == 0) {
// notify the failure on the main thread
notifyError();
return;
}
// we have retrieved our message, notify the UI on the main thread
postMessage(message);
}

WelcomingInteractor run method.

上述命令试图提取消息,并将消息或错误发送到 UI 进行显示。可以通过实际上为 Presenter 的 Callback 来通知 UI。这就是业务逻辑的主要内容。其他需要做的就全都是框架相关的内容了。

该 Interactor 所拥有的依赖关系如下:

复制代码
import com.kodelabs.boilerplate.domain.executor.Executor;
import com.kodelabs.boilerplate.domain.executor.MainThread;
import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor;
import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor;
import com.kodelabs.boilerplate.domain.repository.MessageRepository;

从上面代码可以看出,其中没有涉及到任何的 Android SDK 代码。这也是 Clean 方法的主要优势。从中也可以看出“框架的独立性”这一特征是成立的。而且,你并不需要关心任何特定的 UI 或数据库,只需要调用外层中实现的接口函数即可。因此,“UI 的独立性”和“数据库的独立性”同样成立。

测试刚创建的 Interactor

现在,就可以在不运行模拟器的情况下运行和测试 Interactor。因此,编写一个简单的 JUnit 测试以确保它能够正常工作:

复制代码
...
@Test
public void testWelcomeMessageFound() throws Exception {
String msg = "Welcome, friend!";
when(mMessageRepository.getWelcomeMessage())
.thenReturn(msg);
WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(
mExecutor,
mMainThread,
mMockedCallback,
mMessageRepository
);
interactor.run();
Mockito.verify(mMessageRepository).getWelcomeMessage();
Mockito.verifyNoMoreInteractions(mMessageRepository);
Mockito.verify(mMockedCallback).onMessageRetrieved(msg);
}

同样的,该 Interactor 代码并不知道它即将被置于的 Android 应用的信息。这就证明了特征点的第二项:business 逻辑是可测试的

编写显示层

在干净架构中,显示代码属于外层。它包含了框架相关的代码,用于将 UI 显示给用户。这里设定使用 MainActivity 类来显示欢迎信息。

首先开始编写 Presenter 和 View 接口的代码。View 所需要做的唯一事情就是显示欢迎信息:

复制代码
public interface MainPresenter extends BasePresenter {
interface View extends BaseView {
void displayWelcomeMessage(String msg);
}
}

那么,应用是如何又在哪里启动 Interactor 的呢?所有不是严格与 View 相关的东西都属于 Presenter 类。这就取得了关注点分离的效果,并避免了 Activity 类变得臃肿。

MainActivity 类中我们重载了 onResume() 方法:

复制代码
@Override
protected void onResume() {
super.onResume();
// let's start welcome message retrieval when the app resumes
mPresenter.resume();
}

所有的 Presenter 对象都在扩展 BasePresenter 粗体文本时实现了 resume() 方法。

注意:尽管 Presenter 属于低层,本文还是将 Android lifecycle 的方法添加到了 BasePresenter 接口中。Presenter 不应该知道 UI 层中的任何信息,如它拥有一个 lifecycle。然而,有些时候每一个 UI 都需要展示给用户,我在这里并没有指定 Android 相关的事件。读者想象一下上述代码中使用 onUIShow(),而非 onResume(),即可明白其含义。

启动 Interactor 的工作是通过 resume() 方法中的 MainPresenter 类进行的:

复制代码
@Override
public void resume() {
mView.showProgress();
// initialize the interactor
WelcomingInteractor interactor = new WelcomingInteractorImpl(
mExecutor,
mMainThread,
this,
mMessageRepository
);
// run the interactor
interactor.execute();
}

其中,execute() 方法将只执行后台线程中 WelcomingInteractorImpl 的 run() 方法。run() 方法可以参看“编写一个新的 Interactor”这一节。

读者或许注意到了,Interactor 与 AsyncTask 类的行为类似。它需要所有运行和执行它所需要的东西作为输入。读者或许疑惑,为什么不干脆直接使用 AsyncTask 呢?其原因在于,那是 Android 特定的代码,需要一个模拟器才能运行和测试它。

我们提供以下东西给 Interactor:

  • 负责在后台线程执行 Interactor 的 ThreadExecutor 实例。我通常将其设为单例。该类实际上位于 domain 包内,并不需要在外层进行实现。
  • 负责主线程中可运行内容的 MainThreadImpl 实例。主线程程可以通过框架相关的代码进行访问,因此该实例可以在外层中进行实现。
  • this 也是提供给 Interactor 的东西之一——MainPresenter 是 Interactor 用来通知 UI 相关事件的 Callback 对象。
  • 最后,WelcomeMessageRepository 实例负责实现 Interactor 使用的 MessageRepository 接口。该实例的细节将会在“编写存储层”一节进行展示。

注意:由于一次提供给 Interactor 的内容太多,使用 Dagger 2 这样的依赖注入框架将是很好的选择。但是,本文为简单起见并未引入。读者可以自行决定是否使用这样的框架。

有关 this,MainActivity 的 MainPresenter 真正实现了 Callback 接口:

复制代码
public class MainPresenterImpl extends AbstractPresenter
implements MainPresenter, WelcomingInteractor.Callback {

这也正是应用从 Interactor 监听消息的方法。以下就是 MainPresenter 中的相关方法:

复制代码
@Override
public void onMessageRetrieved(String message) {
mView.hideProgress();
mView.displayWelcomeMessage(message);
}
@Override
public void onRetrievalFailed(String error) {
mView.hideProgress();
onError(error);
}

在这些函数中可见的 View 就是实现这一接口的 MainActivity 类:

复制代码
public class MainActivity extends AppCompatActivity implements MainPresenter.View {

而负责显示欢迎信息的代码如下:

复制代码
@Override
public void displayWelcomeMessage(String msg) {
mWelcomeTextView.setText(msg);
}

以上就是显示层的全部相关内容。

编写存储层

该层实现了 repository 的功能。所有数据库相关的代码都应该在该层中。repository 模式仅仅对数据的来源进行了抽象。业务逻辑就可以不考虑数据的具体来源——是数据库、服务器或者文本文件。

对于复杂的数据,可以使用 ContentProvider 或 DBFlow 等 ORM 工具。如果需要从网页中抓取数据,Retrofit 将会是很好的工具。如果需要简单的键值存储,SharedPreference 会是不错的选择。不同的工作需要根据需求来选择不同的工具。

本文中的数据库将不会是一个真正的数据库。它只是一个拥有一定仿真延迟的类:

复制代码
public class WelcomeMessageRepository implements MessageRepository {
@Override
public String getWelcomeMessage() {
String msg = "Welcome, friend!"; // let's be friendly
// let's simulate some network/database lag
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return msg;
}
}

在我们关注到 WelcomingInteractor 时,延迟可能是实际网络或其他原因导致的。只要 MessageRepository 实现了该接口,其具体内容并不需要关心。

小结

本文的示例可以通过 Git 仓库( https://github.com/dmilicic/Android-Clean-Boilerplate/tree/example )进行访问。类调用的关系总结如下:

MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity

控制流的顺序如下:

Outer — Mid — Core — Outer — Core — Mid — Outer

在一个用例中多次访问外层是很正常的。在需要从网络中显示、存储和访问一些内容的情况下,控制流会至少三次访问外层。

结论

对于我而言,干净架构已经是我至今开发应用中最好的架构模式。解耦合的代码使得你可以把注意力更多的聚焦到特定的问题上。 而且,干净架构不失为一个相当 SOLID 的方法,虽然它可能需要一定时间来习惯它。这也正是我编写该教程,通过一步一步的详细例子来帮助人们理解干净架构的原因。

此外,我还使用干净架构开发并开源了一个消费追踪的示例应用。该应用主要用于展示在实际应用中的代码。尽管该应用并没有很创新的特性,它涵盖了本文所讨论的所有内容,并通过更加复杂的例子进行了演示。你可以通过 Sample Cost Tracker App( https://github.com/dmilicic/android-clean-sample-app )找到相关信息。

该应用同样构建于我之前分享的干净架构样板代码之上,可以通过 Android Clean Boilerplate( https://github.com/dmilicic/Android-Clean-Boilerplate )找到详细信息。

进一步阅读

本教程在一定意义上是对 Fernando Cejas 文章( http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/ )的扩展。二者不同之处在于,本文为了在演示干净时不给读者带来太多负担,采用了常规的 Java 例子。如果读者希望看到 RxJava 的例子,可以参考 Fernando Cejas 的另一篇文章( http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/ )。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-03-16 17:3916629
用户头像

发布了 268 篇内容, 共 123.3 次阅读, 收获喜欢 24 次。

关注

评论

发布
暂无评论
发现更多内容

【Java Web 系列】Session的原理分析和使用细节

倔强的牛角

Java javaWeb session 6月月更

IDM短信发送接口设计说明

agileai

数据安全 集成底座 企业服务总线 统一身份管理平台 主数据管理平台

写代码过程中最忌讳什么?

Jackpop

首批 | 腾讯云完成国内首个云原生安全成熟度评估

腾讯安全云鼎实验室

云原生

量化夹子机器人系统开发逻辑分析

开发微hkkf5566

Nginx 配置和性能调优

CRMEB

Apache Dolphinscheduler3.0.0-beta-1 版本发布,新增FlinkSQL、Zeppelin任务类型

白鲸开源

Apache 大数据 开源 Apache DolphinScheduler 消息队列 workflow

低代码开发:企业应用构建新模式

力软低代码开发平台

服务网格项目Aeraki Mesh正式进入CNCF沙箱

York

开源 云原生 istio Service Mesh 服务网格 cncf

2022年中国社区团购发展新动向

易观分析

社区团购

物联网低代码平台如何添加报警配置?

AIRIOT

物联网 低代码开发 低代码平台

【LeetCode】找到一个数字的 K 美丽值Java题解

Albert

LeetCode 6月月更

Jupyter Notebook,太强大了!

Jackpop

10段实用Python代码,帮我省了几万块钱!

Jackpop

从 keynote 大神到语雀画图大神,她是怎么做到的?

编辑器 思维导图 文档管理 企业知识管理

保险APP适老化服务评测框架 发布

易观分析

保险

InfoQ 极客传媒 15 周年庆征文|Spring Cloud netflix概览及架构设计

No Silver Bullet

架构 6月月更 InfoQ极客传媒15周年庆 Spring Cloud netflix

IT采购专家指导 联想百应618 IT运维软硬服一站集采

Geek_2d6073

从行业角度看,数仓领域的未来是什么?

字节跳动数据平台

字节跳动 数据仓库 OLAP

How to solve the different brightness of LED display colors

Dylan

LED LED display

API工具--Apifox和Postman对比(区别)

Liam

Java 后端 Postman API API接口管理

SQL和Python 哪个更容易自学?

Jackpop

web前端培训VUE开发者需要知道哪些实用技术点

@零度

Vue 前端开发

云GPU的618,拿捏了

恒源云

人工智能 深度学习 GPU服务器

二级等保要求几年做一次测评?测评项目有多少项?

行云管家

等级保护 等保测评 二级等保 等保二级

帮助文档在软件中的存在价值是什么?

小炮

KubeVela 1.4:让应用交付更安全、上手更简单、过程更透明

孙健波

Kubernetes OAM KubeVela 云原生应用 K8s 多集群管理

直播app源码,充值提现模板的设计看似简单却不简单

开源直播系统源码

软件开发 直播源码 app定制开发

7 月亚马逊云科技培训与认证课程,精彩不容错过!

亚马逊云科技 (Amazon Web Services)

架构师 培训 认证

TiDB 6.0 实战分享丨内存悲观锁原理浅析与实践

PingCAP

分布式数据库 TiDB

justcows奶牛理财dapp系统开发

开发微hkkf5566

使用Clean Architecture模式开发Android应用的详细教程_Android/iOS_张天雷_InfoQ精选文章