最近我用 Clean 架构为客户构建了 app,并收到了很好的反馈。因此,在这篇文章中我会手把手教你如何用 Clean 架构开发 Android 应用。
什么是 Clean 架构?
有许多文章已经对 Clean 架构的概念做过介绍。在此我讲一讲 Clean 架构的核心内容。
通常所说的 Clean,是指代码被分为像洋葱状的多个层,其规则基础:内层不需要知道外层在干什么。即向内依赖。
这是上一段内容的直观呈现:
简洁架构极佳的视觉表现。图片来自Uncle Bob。
文中提到的 Clean 架构会给代码提供以下属性:
-
不依赖框架。
-
可测试。
-
不依赖 UI。
-
不依赖数据库。
-
不依赖其它外部力量
这在 Anroid 中意味着什么
一般来说,你的应用可以有任意数量的层,除非你的 Android 应用包含企业级的业务逻辑,最常见的是3层:
外层:实现层
中间层:接口适配层
内层:业务逻辑层
实现层是框架要求所有事情发生的地方。构架代码包括不解决特定问题的代码,比如所有 Android 开发者都喜欢的创建 Activity 和 Fragment,发送 Intent,以及其它网络和数据库相关的框架代码。
接口适配层的目标是连接业务逻辑和框架代码。
最重要的问题是业务逻辑层。这里是你的应用中实际解决问题的地方。这里不会有框架代码,你应该能在没有模拟器支持下运行这部分代码。这样你的业务逻辑代码才容易测试、开发和维护。这是 Clean 架构的主要优势。
核心层之上的每一层都需要为下一层转换模型结构。内层不会引用外层的模型,但外层可以使用内层的模型。这也是前面提到的依赖规则。虽然这样做会导致更大的开销,但能确保各层代码之间的解耦。
为什么需要模型转换?举个例子,当逻辑层的模型不能直接很优雅地展现给用户,或是需要同时展示多个逻辑层的模型时,最好创建一个 ViewModel 类来更好的进行 UI 展示。这样可以在外层使用转换器类将业务模型转换成合适的 ViewModel。
另一个例子:假设你要从外部数据层的 ContentProvider 得到一个 Cursor 对象,外层要先把它转换成内层的业务模型,再送给你的业务逻辑层进行处理。
文末我会给出更多相关资源,以便你了解更多相关信息。现在我们已经了解 Clean 架构的基本原理,接下来我们需要用代码示例进行说明:用 Clean 架构构建一个示例功能。
怎样开始构建一个 Clean 应用?
我做了一个样板项目,它为你提供了所有的底层命令。这是一个 Clean 启动包,在设计之初就包含最常用的一些工具包。你可免费下载和修改,还能用它建立自己的应用程序。
你可以在这里找到入门项目: Android Clean Boilerplate
开始编写新用例
本节将解释所有需要编写的代码,你可通过上一节提供的样板文件使用 Clean 方法创建一个示例。 一个示例只代表应用程序中的部分独立功能。 用户(例如,在点击时)可以选择启用或不启用。
首先我们来解释这种方法的结构和术语。这里要说的是我如何构建应用程序,其方法并不固定,你可根据你的需求组织不同的结构。
结构
一般的 Android 应用结构如下:
外层包:UI、Storage、Network 等。
中层包:Presenters, Converters
内层包:Interactors、Models、Repositories、Executor
外层
上面已经提到过,这里是框架的细节。
UI —包括 Activite、Fragment、Adapter 和其它用户界面相关的代码。
Storage — 数据库相关代码,实现 Interactor 需要使用的接口,用于访问和存储数据。包含如 ContentProviders 或者像 DBFlow 这样的 ORM。
Network — 类似 Retrofit 的网络操作。
中层
粘合代码层,将实现细节与业务逻辑连接起来。
Presenters — 处理来自 UI 的事件(比如用户单击)或者常用作内层(Interactor)的回调。
Converters — 转换器对象负责把内部模型转换为外部模型,反之亦然。
内层
核心层包含大部分高等级代码。这里的所有类都是 POJO。这一层中的类和对象都不是特定运行在 Android 应用中,可以非常容易的移植到其它 JVM 运行。
Interactors - 这些是实际包含业务逻辑代码的类。这些类在后台运行,并使用回调向上层传递事件。在一些项目中,它们也被称为用例(可能是一个合适的名称)。在您的项目中可能有很多小的用于解决特定问题 Interactor 类,这属正常现象。可以说,它符合单一责任原则,而且这样的理解更容易让人接受。
Models - 这些是您在业务逻辑中处理的业务模型。
Repositories - 此包仅包含数据库或其他外层实现的接口。Interactors 使用这些接口来访问和存储数据。也称为仓库模式。
Executor - 此包包含用于调用工作线程执行器在后台执行 Interactors 的代码。这个包一般不需要你修改任何部分。
一个简单的示例
在这个示例中,我们的用例是: “在 app 启动时读取存储在数据库中的消息并展示。“ 此示例将会展示如何使用下面三个程序包来完成用例的功能:
presentation 包(展示包)
storage 包(存储包)
domain 包(主包)
前两个属于外层实现,最后一个属于内部/核心层实现。
Presentation 包主要负责所有与屏幕显示相关的部分——包括全部的 MVP 栈,即包括 UI 和 presenter 这两个不同层的组件。
编写新的 Interactor (内部/核心层)
事实上你可以从架构的任意层开始编码,但是我还是推荐你首先从核心业务逻辑开始。因为逻辑代码写好之后可以测试,不需要 activity 也可以正常运行。
所以我们先从创建一个 Interactor 开始。Interactor 是用例主逻辑实现的地方。所有的 Interactors 都运行在后台线程,因此应该不会对 UI 展示造成影响。 我们在这里新建一个 Interactor,叫做 WelcomingInteractor。
public interface WelcomingInteractor extends Interactor {
interface Callback {
void onMessageRetrieved(String message);
void onRetrievalFailed(String error);
}
}
Callback 负责和主线程中的 UI 交互,我们之所以将其放在 Interactor 接口中是因为我们不需要将其重新命名为 WelcomingInteractorCallback——用于将其与其他回调区分。下面让我们实现取回消息的逻辑。假设我们有一个 Interactor 的 MessageRepository,可以给我们发送欢迎消息。
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 运行方法。
这里尝试获取了数据,并发送消息或者错误码到 UI 层用于显示。我们通过 Callback 通知 UI,这个 Callback 扮演的是 presenter 的角色。这段代码是我业务逻辑的关键。其他框架都是依赖于框架本身。
让我们看一下 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 代码,这就是 Clean 架构的主要好处。你可以看到框架的独立性。 另外,我们不需要关注 UI 或数据库的细节,我们只是调用外层实现的接口方法。
测试 Interactor
现在我们可以脱离仿真器运行并测试 Interator。来写个简单的 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 应用中。这证明了上面提到的第二点——我们的业务逻辑是可测试的。
编写展现层
展现代码属于简洁框架的外层。它由向用户呈现界面的框架代码组成。我们使用 MainActivity 类在用户回到应用的时候向用户显示欢迎信息。
我们从 Presenter 和 View 开始写界面。视图需要干的唯一一件事情就是显示欢迎信息:
public interface MainPresenter extends BasePresenter {
interface View extends BaseView {
void displayWelcomeMessage(String msg);
}
}
那么,用户回到应用的时候,应该如何开始 Interactor 呢?一切不严格相关的东西都应该放在 Presenter 类中。这有助于组织离散的关系并防止 Activity 变得臃肿。这包括所有用 Interator 运行的代码。
在 MainActivity 类中重载 onResume() 方法:
@Override
protected void onResume() {
super.onResume();
// let's start welcome message retrieval when the app resumes
mPresenter.resume();
}
所有 Presenter 对象都要在实现 BasePresenter 的时候实现 resume() 方法。
注意:有些敏锐的读者会发现我在 BasePresenter 接口中添加了 Android 的生命周期方法,即使 Presenter 在较低层。Presenter 不会获知 UI 层的任何内容——比如它的生命周期。然而,我并没有指定 Android 特定的 * 事件* ,因为每个 UI 都需要向用户展示。想像一下,我调用的是 onUIShow() 而不是 onResumt(),结果会怎么样呢。一切运行良好,不是吗?:)
所有的 Presenter 在继承 BasePresenter 时都要实现 resume() 方法。我们在 MainPresenter 的 Resume() 方法中启动 Interactor。
@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 代码,需要模拟器才能运行或测试。
我们为 Interfactor 提供下列属性:
ThreadExecutor 实例负责在后台线程中执行 Interactor。我通常会使用单例模式。这个类实际驻留在域包中,不需要在外层实现。
MainThreadImpl 实例负责在主线程上从 Interactor 发送可运行对象。主线程可以使用框架代码访问,因此这个类需要在外层实现。
你可能注意到我们向 Interactor 提供了 this ,因为 MainPresenter 也是一个 Callback 对象,Interactor 会用它在事件回调中更新 UI。
WelcomeMessageRepository 实现了 Interactor 用到的 MessageRepository 接口,所以我们提供了它的实例。WelcomeMessageRepository 会在编写存储层 一节中详述。
注意:因为每次都需要向 Interactor 提供许多属性,将 Dagger 2 依赖注入框架会提供不少帮助。简明起见,此处没有将其注入。你可根据实际情况选择使用。
为什么 this 也是 Callback 呢?,因为 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 其实就是实现了 MainPresenter.View 接口的 MainActivity:
public class MainActivity extends AppCompatActivity implements MainPresenter.View {
它负责显示欢迎信息:
@Override
public void displayWelcomeMessage(String msg) {
mWelcomeTextView.setText(msg);
}
这差不多就是表示层的内容了。
编写存储层
repository 中的接口就在存储层实现。所有数据库相关的代码都在这里。仓库模式只是表达数据来源。但我们的主要业务逻辑不在乎首数据的来源——不管它是来自数据库、服务器还是文本文件。
对于复杂的数据,你可以使用 ContentProviders 或者像 DBFlow 这样的 ORM 工具处理。如果你需要从 Web 接收数据,那就会用到 Retrofit。如果你需要简单的键值对存储,那你会用到 SharedPreferences。不管怎样,你需要选择正确的工具。
我们的数据库并不是真正的数据库,它只是一个简单的类,通过延迟来模拟:
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 接口就好。
概览
这个示例已经放在GitHub上。各个类之间的调用关系总结如下:
MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity
注意这个控制流程,这非常重要:
Outer — Mid — Core — Outer — Core — Mid — Outer
在一个用例中多次访问外层是很常见的事情。如果你要显示点什么,存储点什么并从 Web 访问些什么,控制流至少需要访问外层三次。
结论
对于我来说,这是迄今为止开发应用程序的最佳方式。解耦的代码能让人把注意力放在具体的问题上,而不受其他事件干扰。这是一个不错的 SOLID 方法,但我们还需要一些时间适应。希望这篇文章的示例能让你对该内容有进一步了解。
我还使用 Clean 架构建立了一个开源的成本跟踪应用,它能展示一项应用的编码。此应用并无创新内容,你若感兴趣,可查看:成本跟踪应用示例
另外,此示例 app 是根据简洁的启动包创建,你可以在这里找到相关信息:Android Clean Boilerplate
相关阅读
本指南是对之前文章的扩展。区别在于我在示例中使用了常规的 Java 代码,以避免在展示这个方法的时候有其它的影响。如果你想看到在简洁架构中使用 RxJava 的例子,可以看看这个。