如何用 Clean 架构开发 Android 应用(A detailed guide on developing Android apps using the Clean Architecture pattern)

By | 2018年7月12日

Ever since I
started developing Android apps there was this feeling that it could be
done better. I’ve seen a lot of bad software design decisions during my
career, some of which were my own?—?and Android complexity mixed with
bad software design is a recipe for disaster. But it is important to
learn from your mistakes and keep improving. After a lot of searching
for a better way to develop apps I encountered the Clean Architecture.
After applying it to Android, with some refinement and inspiration from
similar projects, I decided that this approach is practical enough and
worth sharing.

The goal
of this article is to provide a step-by-step guide for developing
Android apps in a Clean way. This whole approach is how I’ve recently
been building my apps for clients with great success.

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

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

What is Clean Architecture?

I
will not go into too much detail here as there are articles that
explain it much better than I can. But the next paragraph provides the crux of what you need to know to understand Clean.

Generally in Clean, code is separated into layers in an onion shape with one dependency rule: The inner layers should not know anything about the outer layers. Meaning that the dependencies should point inwards.

This is the previous paragraph visualized:

Awesome visual representation of the Clean Architecture. All credit for this image goes to Uncle Bob.

Clean Architecture, as mentioned in the provided articles, makes your code:

  • Independent of Frameworks

  • Testable.

  • Independent of UI.

  • Independent of Database.

  • Independent of any external agency.

I
will hopefully make you understand how these points are achieved with
examples below. For a more detailed explanation of Clean I really
recommend this article and this video.

什么是 Clean 架构?

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

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

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

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

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

  • 不依赖框架。

  • 可测试。

  • 不依赖 UI。

  • 不依赖数据库。

  • 不依赖其它外部力量

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

What this means for Android

Generally,
your app can have an arbitrary amount of layers but unless you have
Enterprise wide business logic that you have to apply in every Android
app, you will most often have 3 layers:

  • Outer: Implementation layer

  • Middle: Interface adapter layer

  • Inner: Business logic layer

The implementation layer is where everything framework specific happens. Framework specific code includes every line of code that is not solving the problem you set to solve,
this includes all Android stuff like creating activities and fragments,
sending intents, and other framework code like networking code and
databases.

The purpose of the interface adapter layer is to act as a connector between your business logic and framework specific code.

The most important layer is the business logic layer.
This is where you actually solve the problem you want to solve building
your app. This layer does not contain any framework specific code and
you should be able to run it without an emulator. This way you can have your business logic code that is easy to test, develop and maintain. That is the main benefit of the Clean Architecture.

这在 Anroid 中意味着什么

一般来说,你的应用可以有任意数量的层,除非你的 Android 应用包含企业级的业务逻辑,最常见的是3层:

  • 外层:实现层

  • 中间层:接口适配层

  • 内层:业务逻辑层

实现层是框架要求所有事情发生的地方。构架代码包括不解决特定问题的代码,比如所有 Android 开发者都喜欢的创建 Activity 和 Fragment,发送 Intent,以及其它网络和数据库相关的框架代码。

接口适配层的目标是连接业务逻辑和框架代码。

最重要的问题是业务逻辑层。这里是你的应用中实际解决问题的地方。这里不会有框架代码,你应该能在没有模拟器支持下运行这部分代码。这样你的业务逻辑代码才容易测试、开发和维护。这是 Clean 架构的主要优势。

Each
layer, above the core layer, is also responsible for converting models
to lower layer models before the lower layer can use them. An inner
layer can not have a reference to model class that belongs to the outer
layer. However, the outer layer can use and reference models from the
inner layer. Again, this is due to our dependency rule. It does create overhead but it is necessary for making sure code is decoupled between layers.

Why is this model conversion necessary?
For example, your business logic models might not be appropriate for
showing them to the user directly. Perhaps you need to show a
combination of multiple business logic models at once. Therefore, I
suggest you create a ViewModel class that makes it easier for you to
display it to the UI. Then, you use a converter class in the outer layer to convert your business models to the appropriate ViewModel.

Another example might be the following: Let’s say you get a Cursor object from a ContentProvider
in an outer database layer. Then the outer layer would convert it to
your inner business model first, and then send it to your business logic
layer to be processed.

I
will add more resources to learn from at the bottom of the article. Now
that we know about the basic principles of the Clean Architecture,
let’s get our hands dirty with some actual code. I will show you how to
build an example functionality using Clean in the next section.

核心层之上的每一层都需要为下一层转换模型结构。内层不会引用外层的模型,但外层可以使用内层的模型。这也是前面提到的依赖规则。虽然这样做会导致更大的开销,但能确保各层代码之间的解耦。

为什么需要模型转换?举个例子,当逻辑层的模型不能直接很优雅地展现给用户,或是需要同时展示多个逻辑层的模型时,最好创建一个 ViewModel 类来更好的进行 UI 展示。这样可以在外层使用转换器类将业务模型转换成合适的 ViewModel。

   
另一个例子:假设你要从外部数据层的 ContentProvider 得到一个 Cursor 对象,外层要先把它转换成内层的业务模型,再送给你的业务逻辑层进行处理。

文末我会给出更多相关资源,以便你了解更多相关信息。现在我们已经了解 Clean 架构的基本原理,接下来我们需要用代码示例进行说明:用 Clean 架构构建一个示例功能。

How do I start writing Clean apps?

I’ve made a boilerplate project that has all of the plumbing written for you. It acts as a Clean starter pack and is designed to be built upon immediately with most common tools included from the start. You are free to download it, modify it and build your apps with it.

You can find the starter project here: Android Clean Boilerplate

Getting started writing a new use case

This
section will explain all the code you need to write to create a use
case using the Clean approach on top of the boilerplate provided in the
previous section. A use case is just some isolated functionality of the
app. A use case may (e.g. on user click) or may not be started by a
user.

First let’s explain the structure and terminology of this approach. This is how I build apps but it is not set in stone and you can organize it differently if you want.

怎样开始构建一个 Clean 应用?

我做了一个样板项目,它为你提供了所有的底层命令。这是一个 Clean 启动包,在设计之初就包含最常用的一些工具包。你可免费下载和修改,还能用它建立自己的应用程序。

你可以在这里找到入门项目: Android Clean Boilerplate

开始编写新用例

本节将解释所有需要编写的代码,你可通过上一节提供的样板文件使用 Clean 方法创建一个示例。 一个示例只代表应用程序中的部分独立功能。 用户(例如,在点击时)可以选择启用或不启用。

首先我们来解释这种方法的结构和术语。这里要说的是我如何构建应用程序,其方法并不固定,你可根据你的需求组织不同的结构。

Structure

The general structure for an Android app looks like this:

  • Outer layer packages: UI, Storage, Network, etc.

  • Middle layer packages: Presenters, Converters

  • Inner layer packages: Interactors, Models, Repositories, Executor

Outer layer

As already mentioned, this is where the framework details go.

UI?—?This is where you put all your Activities, Fragments, Adapters and other Android code related to the user interface.

Storage?—?Database
specific code that implements the interface our Interactors use for
accessing data and storing data. This includes, for example, ContentProviders or ORM-s such as DBFlow.

Network?—?Things like Retrofit go here.

Middle layer

Glue code layer which connects the implementation details with your business logic.

Presenters?—?Presenters handle events from the UI (e.g. user click) and usually serve as callbacks from inner layers (Interactors).

Converters?—?Converter objects are responsible for converting inner models to outer models and vice versa.

Inner layer

The core layer contains the most high-level code. All classes here are POJOs.
Classes and objects in this layer have no knowledge that they are run
in an Android app and can easily be ported to any machine running JVM.

结构

一般的 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?—?These are the classes which actually contain your business logic code.
These are run in the background and communicate events to the upper
layer using callbacks. They are also called UseCases in some projects
(probably a better name). It is normal to have a lot of small Interactor
classes in your projects that solve specific problems. This conforms to
the Single Responsibility Principle and in my opinion is easier on the brain.

Models?—?These are your business models that you manipulate in your business logic.

Repositories?—?This
package only contains interfaces that the database or some other outer
layer implements. These interfaces are used by Interactors to access and
store data. This is also called a repository pattern.

Executor?—?This
package contains code for making Interactors run in the background by
using a worker thread executor. This package is generally not something
you need to change.

Interactors – 这些是实际包含业务逻辑代码的类。这些类在后台运行,并使用回调向上层传递事件。在一些项目中,它们也被称为用例(可能是一个合适的名称)。在您的项目中可能有很多小的用于解决特定问题 Interactor 类,这属正常现象。可以说,它符合单一责任原则,而且这样的理解更容易让人接受。

Models – 这些是您在业务逻辑中处理的业务模型。

Repositories? – 此包仅包含数据库或其他外层实现的接口。Interactors 使用这些接口来访问和存储数据。也称为仓库模式

Executor – 此包包含用于调用工作线程执行器在后台执行 Interactors 的代码。这个包一般不需要你修改任何部分。

A simple example

In this example, our use case will be: “Greet the user with a message when the app starts where that message is stored in the database.” This example will showcase how to write the following three packages needed to make the use case work:

  • the presentation package

  • the storage package

  • the domain package

The first two belong to the outer layer while the last one is the inner/core layer.

Presentation package is responsible for everything related to showing things on the screen?—?it includes the whole MVP stack (it means it also includes both the UI and Presenter packages even though they belong to different layers).

OK?—?less talk, more code.

一个简单的示例

在这个示例中,我们的用例是: “在 app 启动时读取存储在数据库中的消息并展示。“ 此示例将会展示如何使用下面三个程序包来完成用例的功能:

  • presentation 包(展示包)

  • storage 包(存储包)

  • domain 包(主包)

前两个属于外层实现,最后一个属于内部/核心层实现。

Presentation 包主要负责所有与屏幕显示相关的部分——包括全部的 MVP 栈,即包括 UI 和 presenter 这两个不同层的组件。

Writing a new Interactor (inner/core layer)

In
reality you could start in any layer of the architecture, but I
recommend you to start on your core business logic first. You can write
it, test it and make sure it works without ever creating an activity.

So let’s start by creating an Interactor. The Interactor is where the main logic of the use case resides. All Interactors are run in the background thread so there shouldn’t be any impact on UI performance. Let’s create a new Interactor with a warm name of WelcomingInteractor.

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

The Callback is
responsible for talking to the UI on the main thread, we put it into
this Interactor’s interface so we don’t have to name it a WelcomingInteractorCallback?—?to distinguish it from other callbacks. Now let’s implement our logic of retrieving a message. Let’s say we have a MessageRepository that can give us our welcome message.

  MessageRepository { 
    String getWelcomeMessage();
}

编写新的 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();
}

Now let’s implement our Interactor interface with our business logic. It is important that the implementation extends the AbstractInteractor which takes care of running it on the background thread.

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.

This just attempts to retrieve the message and sends the message or
the error to the UI to display it. We notify the UI using our Callback
which is actually going to be our Presenter.
That is the crux of our business logic. Everything else we need to do is framework dependent.

Let’s take a look which dependencies does this Interactor have:

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;

As you can see, there is no mention of any Android code. That is the main benefit of this approach. You can see that the Independent of Frameworks point
holds. Also, we do not care about specifics of the UI or database, we
just call interface methods that someone somewhere in the outer layer
will implement. Therefore, we are Independent of UI and Independent of Databases.

下面让我们参考业务逻辑实现 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 或数据库的细节,我们只是调用外层实现的接口方法。


via:oschina

发表评论

电子邮件地址不会被公开。 必填项已用*标注