如何用 Clean 架构开发 Android 应用

【翻译】自我开始开发 Android 应用以来就有一种感觉——我可以把它做得更好。在我的职业生涯中,我看到过不少烂代码,其中一些还是我自己写的。Android 的复杂性和烂代码势必造成大问题。所以,从错误中汲取教训并持续改善十分重要。在多次尝试寻找更好的开发方式后,我遇到了 Clean 架构(简洁架构)。于是我将其应用在了 Android 开发中,并结合我的开发经验做了调整,写出了这篇我觉得较为实用、值得分享的文章。

英文原文:A detailed guide on developing Android apps using the Clean Architecture pattern

来自开源中国
原文链接: 如何用 Clean 架构开发 Android 应用
参与翻译 (5人) : 边城, Tocy, Viyi, 紫系流月, 无若

最近我用 Clean 架构为客户构建了 app,并收到了很好的反馈。因此,在这篇文章中我会手把手教你如何用 Clean 架构开发 Android 应用。

什么是 Clean 架构?

有许多文章已经对 Clean 架构的概念做过介绍。在此我讲一讲 Clean 架构的核心内容。

通常所说的 Clean,是指代码被分为像洋葱状的多个层,其规则基础:内层不需要知道外层在干什么。即向内依赖

这是上一段内容的直观呈现:


简洁架构极佳的视觉表现。图片来自Uncle Bob

文中提到的 Clean 架构会给代码提供一下属性:

  • 不依赖框架。
  • 可测试。
  • 不依赖 UI。
  • 不依赖数据库。
  • 不依赖其它外部力量

我希望你能理解这几点在下面的示例中是如何体现的。更多关于 Clean 架构的解释,我推荐你看看这篇文章和这个视频

这在 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 类在用户回到应用的时候向用户显示欢迎信息。

我们从 PresenterView 开始写界面。视图需要干的唯一一件事情就是显示欢迎信息:

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() 方法。我们在 MainPresenterResume() 方法中启动 Interactor。

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

execute() 方法会在后台线程中执行 WelcomingInteractorImplrun() 方法。而 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 呢?,因为 MainActivityMainPresenter 实现了 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 方法,但我们还需要一些时间适应。希望这篇文章的示例能让你对该内容有进一步了解。

使用简洁架构,我还建立了一个开源的成本跟踪应用,它能展示一项应用的编码。此应用并无创新内容,你若感兴趣,可查看:成本跟踪应用示例

另外,此示例 app 是根据简洁的启动包创建,你可以在这里找到相关信息:Android Clean Boilerplate

相关阅读

本指南是对该文章的扩展。区别在于我在示例中使用了常规的Java代码,以避免在展示这个方法的时候有其它的影响。如果你想看到在简洁架构中使用 RxJava 的例子,可以看看这个

文章目录
  1. 1. 什么是 Clean 架构?
  2. 2. 这在 Anroid 中意味着什么
  3. 3. 怎样开始构建一个 Clean 应用?
  4. 4. 开始编写新用例
    1. 4.1. 结构
    2. 4.2. 外层
    3. 4.3. 中层
    4. 4.4. 一个简单的示例
  5. 5. 编写新的 Interactor (内部/核心层)
  6. 6. 测试 Interactor
  7. 7. 编写展现层
  8. 8. 编写存储层
  9. 9. 概览
  10. 10. 结论
  11. 11. 相关阅读
|