设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 开源资讯 查看内容

如何用Clean架构开发Android应用

2016-12-23 22:07| 发布者: joejoe0332| 查看: 1053| 评论: 0|原作者: 边城, Tocy, Viyi, 紫系流月, 无若|来自: oschina

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

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

我们从 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 MainPresenterWelcomingInteractor.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 的例子,可以看看这个


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部