使用Clean Architecture模型开发Android应用详细指南

发表于 4年以前  | 总阅读数:1360 次

使用Clean Architecture模型开发Android应用详细指南

使用Clean Architecture模型开发Android应用详细指南

自从我开始开发Android应用就有这个感觉,它可以做得更好。在我的职业生涯中我见过很多糟糕的软件设计决策,有一些是我自己的 — 安卓系统的复杂性和糟糕的软件设计是一个灾难。但重要的是从你的错误中学习并且不断完善。在搜索了大量的好的方式开发应用后我找到了Clean Architecture。把它应用到Android后,从类似的工程中找到一些细化和灵感,我认为这种方法是可行的并且值得分享。

这篇文章的目标是用Clean方法一步一步地指导开发Android应用。这个方法是我最近怎样构建我的应用客户端并取得巨大成功的。

什么是Clean Architecture?

我不会讲太多的细节,这里有篇文章比我解释得更好。但下一段落是你对于理解Clean需要知道的关键所在

通常在Clean模型下,代码用一个依赖规则分离到洋葱状的层:内层不应该知道任何关于外层的东西。意思就是说依赖应该指向里面

这是上一段落的可视化图形:

Clean Architecture的很棒的可视化的表现。图片来自Uncle Bob

Clean Architecture,在提供的文章中提到可以让你的代码:

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

通过下面的例子我希望你可以理解这些特点是怎样实现的。对于Clean更详细的说明我真心推荐这篇文章和这个视频

对于Android来说意味着什么

一般地,你的应用可以有任意数量的层,但除非你有企业级的业务逻辑以便适用于每个Android应用,大部分情况下会有3层:

  • 外层:Implementation layer(实现层)
  • 中层:Interface adapter layer(接口适配层)
  • 内层:Business logic layer(业务逻辑层)

实现层是框架内所有具体代码出现的地方。框架的具体代码包括你设置解决的问题但没有解决的所有代码行,这包括所有像创建activities 和 fragments,发送intents,和别的框架代码像访问网络代码和数据库等。

接口适配层的目的是作为你的业务逻辑和框架具体代码之间的一个连接器。

最重要的层是业务逻辑层。这是构建你的应用解决你实际上想解决的问题的地方。这层不包含任何框架的具体代码并且你应该可以不用在模拟器上运行它。这样的话你才可以有方便测试,开发和维护的业务逻辑代码。这也是Clean Architecture的主要优点。

核心层上的每层,在低层可以使用它们之前负责转换模型到低层模型。内层不能有属于外层的模型类的引用。然而,外层可以使用和引用内层的模型。这是因为我们的依赖规则。它做了创建的开销但是有必要确保代码层之间的解耦。

为什么这个模型转换是必要的? 例如,你的业务逻辑模型直接显示给用户是不合适的。也许你需要展示多个业务逻辑模型的组合。因此,我建议你创建一个ViewModel类让它更方便地展示到UI上。然后,在外层使用_converter_类转换业务模型到合适的ViewModel。

另一个例子可能是这样的:假设你在外部数据库层从ContentProvider得到一个Cursor对象。然后外层首先将转换它到内部业务模型,再然后发送它到你的业务逻辑层进行处理。

在文章末尾我将添加更多的资源来学习。现在我们知道了Clean Architecture的基本原则,让我们来动手敲一些代码吧。在下一部分我将给你展示怎样使用Clean构建一个功能性的例子。

我该怎样写Clean应用?

我已经做了一个模板工程,给你写好了所有必需的东西。它作为一个Clean启动包并且从一开始它就被设计建立在包含最常见的工具之上。你可以免费下载它,修改它和用它构建自己的应用。

你可以从这找到启动工程:Android Clean Boilerplate

编写一个新的用例

这部分将会说明在之前的部分提供的模板上面使用Clean 方法创建一个用例需要写的所有代码。一个用例只是应用的一些分离的功能。一个用例可能(用户点击等)或可能不是由用户启动。

首先,让我们解释一下这个方法的结构和术语。这就是我怎样构建应用的,但它不是固定的,如果你想的话你可以组织不同的结构。

结构

Android应用程序的总体结构是这样的:

  • 外层包:UI,存储,网络,等等。
  • 中层包: Presenters, Converters
  • 内层包:Interactors, Models, Repositories, Executor

外层

刚才已经提到了,这是框架的细节所在。

UI —  这是放置所有的Activities, Fragments, Adapters 和别的用户界面相关的代码。

Storage —  实现访问数据和存储数据交互接口的数据库的具体代码。这包括,例如,ContentProviders或ORM-s 如DBFlow

Network —  比如像Retrofit

中间层

粘合用来连接实现细节和你的业务逻辑的代码层。

Presenters —  Presenters处理UI事件(用户点击等)和通常作为内层的回调(交互器)。

Converters —  Converter对象负责转换内部模型到外部模型,反之亦然。

内层

核心层包含最高级的代码。所有的类都是简单的Java对象。这层的类和对象不知道它们运行在Android应用并且可以很容易地移植到运行JVM的任何机器。

Interactors —  真实包含你的业务逻辑代码的类。这些类运行在后台,通过回调和上层进行通讯。它们也会在一些工程(可能是一个更好的名字)中被用例调用。在你的工程中有很多小的交互类用来解决特定的问题是很正常的。这符合单一职责原则并且在我看来这更方便维护。

Models —  这是你的业务模型用来操作业务逻辑。

Repositories —  这个包只包含数据库或其他一些外层实现的接口。这些接口被用来通过Interactors访问和存储数据。这也被称为repository pattern

Executor —  这个包包含通过使用一个工作线程使Interactors运行在后台的代码。这个包里面的代码通常不需要修改。

简单的例子

在这个例子中,我们的需求如下:当应用启动时,从数据库中读取出消息用来欢迎用户。 这个示例将展示如何编写以下三个包需要的代码让用例工作:

  • presentation
  • storage
  • domain

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

Presentation 包负责一切在屏幕上显示的相关东西—它包含完整的MVP堆(它意味着它也包括 UI 和Presenter包即使它们属于不同的层)。

OK—费话少说,上代码。

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

在实际开发中你可以先写结构的任何层,但我推荐你先写你的核心业务逻辑。你可以写它,测试并在不创建activity时确保它是没问题的。

让我们先创建一个Interactor。Interactor是存放用例的主要逻辑的地方。所有的Interactors运行在后台因此这不应该对UI性能有任何的影响。创建一个名为WelcomingInteractor新的Interactor。

public interface WelcomingInteractor extends Interactor { 

    interface Callback { 

        void onMessageRetrieved(String message);

        void onRetrievalFailed(String error);
    } 
}

Callback负责在主线程和UI通讯,我们把它放在Interactor的接口里因此我们不得不给它一个WelcomingInteractorCallback名称—为了和别的callback进行区分。现在让我们实现我们的获取信息的逻辑。我们有一个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的运行方法。

这只是尝试接收信息和发送信息或错误显示到UI上。我们使用回调通知UI实际上是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代码。这也是这个方法的主要优点。你可以看到框架的独立特点。还有,我们不关心具体的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应用中。这可以证明我们的业务逻辑是可测试的,这正是第二个特点。

编写presentation层

在Clean结构中Presentation代码属于外层。它由展示给用户的UI的框架依赖代码组成。当应用可见时我们将会使用MainActivity 类展示欢迎信息给用户。

让我们开始写PresenterView 的接口。我们的view唯一需要做的事情就是显示欢迎信息:

public interface MainPresenter extends BasePresenter { 

    interface View extends BaseView { 
        void displayWelcomeMessage(String msg);
    } 
}

当应用可见时我们应该怎样和在哪进行交互呢?所有和视图完全无关的应该放到Presenter类。这个会帮助我们实现关系分离并且可以防止Activity类臃肿。

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

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

所有继承BasePresenter的Presenter对象需要实现resume()方法。

注意:聪明的读者可能已经看到了我在BasePresenter接口添加了Android生命周期的方法作为帮助方法,尽管Presenter在低层。它不应该知道任何关于UI层的东西—例如,它有一个生命周期。然而,当每个UI显示给用户时我并没有指定Android具体的事件。假如我叫它onUIShow() 而不是onResume()。是不是好多了?:)

我们在MainPresenter类的resume()方法内进行交互:

@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特有代码 并且你需要在模拟器上运行和测试它。

在interactor里面我们提供了几个类:

  • ThreadExecutor实例负责在后台线程执行交互。我通常把它弄成一个单例。这个类实际上在domain 包下并且不需要在外层实现。

  • MainThreadImpl实例负责从Interactor发送runnables 在主线程中。可以使用框架具体的代码访问主线程因此我们应该在外层实现它。

  • 你可能也注意到了我们给Interactor提供了thisMainPresenter 是Interactor的回调对象用来通知UI事件。

  • 我们提供了一个实现 MessageRepository 接口的WelcomeMessageRepository实例用来交互。稍后在编写存储层部分会覆盖WelcomeMessageRepository

注意:有很多东西每次都需要提供Interactor,依赖注入框架像Dagger 2 会有用的。为了简单起见在这里我没有包含它。实现这样一个框架是留给你的自由。

关于这一点,MainActivityMainPresenter实际上实现了回调接口:

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);
}

我们看到的这些代码段是在我们的MainActivity实现了这个接口:

public class MainActivity extends AppCompatActivity implements MainPresenter.View {

然后负责显示欢迎消息,如下代码所示:

@Override 
public void displayWelcomeMessage(String msg) {
    mWelcomeTextView.setText(msg);
}

并且这几乎就是表现层。

编写存储层

这是实现仓库的地方。所有数据库特定的代码应该放在这。仓库模型只是把数据来源给抽象了。对于数据源来说是不知道我们主业务逻辑的—来自数据库,服务器或文本文件。

对于复杂的数据可以使用ContentProviders或ORM工具例如DBFlow。如果你需要从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上显示,存储和访问一些数据,你的控制流至少访问外层3次。

结论

对于我来说,目前这是开发应用的最好方式。解耦代码便于集中你的注意力在具体问题上并且不会让软件变得臃肿。毕竟,我认为这是一个相当SOLID(可靠的)方法但它需要一些时间去适应。这也是我写这些的原因,通过循序渐进的例子来帮助大家更好的理解。如果还有什么不清楚我很乐意解决这些问题,你的反馈对我来说很重要。我也很想听听哪些可以改善。一个健康的讨论将有利于所有的人。

我也用Clean开发并开源了一个cost tracker应用,展示了如何看起来像一个真正的应用程序的代码。就特性而言真的没有什么创新但我认为它涵盖了我谈到的一个更复杂的例子,你可以从这找到: Sample Cost Tracker App

再次提醒,示例应用构建在Clean启动包之上,可以从这找到:Android Clean Boilerplate

扩展阅读

这个指南是在这篇很棒的文章之上进行了扩展。不同之处是我在例子中使用了普通Java并且没有添加太多重写展示这个方法。如果你想要用Clean的RxJava的例子可以从这里看看。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520762次阅读
Android 深色模式适配原理分析 3年以前  |  28747次阅读
Android阴影实现的几种方案 1年以前  |  10920次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9672次阅读
 目录