Android APP Architecture

澳门新葡亰网站注册 4

自从开始开发安卓应用,我一直感觉我可以做得更好。我看过不少烂代码,其中当然有我写的。安卓系统的复杂性加上烂代码势必酿成灾祸,所以从错误中成长就很重要。我Google了如何更好地开发应用,发现了这个叫做Clean架构的东西。于是我尝试将它应用于安卓开发,根据我在类似项目中的经验做了一些改善,写出了这篇我觉得较为实用、值得分享的文章。

老板让我搭建一个APP,我该怎么快速上手?现在常用的Android应用总体架构是什么样的?Android开发现在有哪些流行的新技术和工具,可以提高我工作效率?这里介绍这样一个工程模板,让你快速搭建健壮,易扩展,易测试,易维护(maintainable)的Android工程。什么是架构本文主要讨论Android软件架构,那么何谓软件架构?软件体系结构是构建计算机软件实践的基础。与建筑师设定建筑项目的设计原则和目标,作为绘图员画图的基础一样,软件架构师或者系统架构师陈述软件架构以作为满足不同客户需求的实际系统设计方案的基础。软件的架构是后续软件开发实施的规则和骨架,好的架构可以极大地降低软件的开发成本和维护成本。

我会在这篇文章中手把手教你在Android应用中使用Clean架构。我最近一直用这种方式优雅地编写应用。

Android应用架构在一直演进,没有最好的架构,只有最适合的架构。通过对历史过程中的架构的了解,可以了解每个架构的优缺点和适用范围,知道为什么我们经历了这些变化。

什么是Clean架构?

有许多文章已经很好地回答了这个问题。我在这里讲一讲Clean架构的核心概念。

一般来说,在Clean架构中,代码被分层成洋葱形,层层包裹,其中有一个依赖性规则:内层不能依赖外层,即内层不知道有关外层的任何事情,所以这个架构是向内依赖的。看个图感受一下:

澳门新葡亰网站注册 1
图片由Bob大叔提供

Clean架构可以使你的代码有如下特性:

  1. 独立于架构
  2. 易于测试
  3. 独立于UI
  4. 独立于数据库
  5. 独立于任何外部类库

我将通过下面的例子解释这些特性是怎么来的。如果你想深入了解Clean架构,不妨看这篇文章和这个视频

MVC模式最早由Trygve
Reenskaug在1978年提出。目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。它将软件系统分为三个部分:模型,视图和控制器。

Clean在Android中如何表现

一般来说,一个应用可以有任意数目的层,但除非你的应用到处是企业级功能逻辑,一般需要这三层:

  • 外层:实现层
  • 中层:接口适配层
  • 内层:逻辑层

接口实现层是体现架构细节的地方。实现架构的代码是所有不用来解决问题的代码,这包括所有与安卓相关的东西,比如创建Activity和Fragment,发送Intent以及其他联网与数据库的架构相关的代码。

添加接口适配层的目的就是桥接逻辑层和架构层的代码。

最重要的是逻辑层,这里包含了真正解决问题的代码。这一层不包含任何实现架构的代码,不用模拟器也应能运行这里的代码。这样一来你的逻辑代码就有了易于测试、开发和维护的优点。这就是Clean架构的一个主要的好处。

每一个位于核心层外部的层都应能将外部模型转成可以被内层处理的内部模型。内层不能持有属于外层的模型类的引用。这也是由于刚才说的依赖性规则,这样内外层可以很好地分离。

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

再举一个例子:你从外部数据库层获得了ContentProvider的Cursor对象,外层首先要将这个对象转换成内层模型,再将它传给内层处理。

在文章的最后我还提供了一些学习资源。我们已经知道了Clean架构的基本原则,现在我们来实践一下。我会在下一部分中使用Clean架构构建一个示例功能。

澳门新葡亰网站注册 2MVC结构图

如何开始写Clean应用?

我已经写好了一个样板项目,里面把准备工作做好了。这相当于是一个Clean的底层包,可以直接在它的基础上进行开发。请随意下载、修改。项目包:Android
Clean
Boilerplate

  • View:对应于xml布局文件
  • Model:模型数据
  • Controllor:对应于Activity业务逻辑,数据处理和界面相应

开始写用例

这一部分会详细说明如何用在样例项目的基础之上以Clean方式进行开发。首先让我们看一下应用的结构,当这只是我的习惯,不需要完全按这个进行。

这里引用一段苹果开发者网站对MVC的论述

结构

一般来说一个安卓应用的结构如下:

  • 外层项目包:UI,Storage,Network等等。
  • 中层项目包:Presenter,Converter。
  • 内层项目包:Interactor,Model,Repository,Executor。

看不懂不要紧,下面有具体解释。

However, there is a theoretical problem with this design. View objects
and model objects should be the most reusable objects in an
application. View objects represent the “look and feel” of an
operating system and the applications that system supports;
consistency in appearance and behavior is essential, and that requires
highly reusable objects. Model objects by definition encapsulate the
data associated with a problem domain and perform operations on that
data. Design-wise, it’s best to keep model and view objects separate
from each other, because that enhances their reusability.

外层

外层体现了框架的细节。

UI
包括所有的Activity,Fragment,Adapter和其他UI相关的Android代码。

Storage
用于让交互类获取和存储数据的接口实现类,包含了数据库相关的代码。包括了如ContentProvider或DBFlow等组件。

Network – 网络操作。

意思大致是:View和Model都是需要重用的模块,在MVC中View监听Model的通知导致View依赖了Model,降低了View的可重用性。现实中常常在Android上实现的MVC里,View是xml文件,Activity是Controller其中又包含了大量和Model数据耦合的代码又要响应用户事件,导致承担功能过多,代码量过大难以维护和测试。而且如果修改了Model的代码,也可能会影响Activity中的逻辑。所以大型的工程,往往需要将Activity中一些和界面无关的职责拆分出来,提高工程的可维护性和可测试性。

中层

桥接实现代码与逻辑代码的Glue Code。

Presenter
presenter处理UI事件,如单击事件,通常包含内层Interactor的回调方法。

Converter – 负责将内外层的模型互相转换。

为了彻底的将Model和View解耦,MVP对MVC进行了一些改进,将View和Model之间的通知和调用去掉,所有的数据流都经过Presenter。

内层

内层包含了最高级的代码,里面都是POJO类,这一层的类和对象不知道外层的任何信息,且应能在任何JVM下运行。

Interactor
Interactor中包含了解决问题的逻辑代码。这里的代码在后台执行,并通过回调方法向外层传递事件。在其他项目中这个模块被称为用例Use
Case。一个项目中可能有很多小Interactor,这符合单一职责原则,而且这样更容易让人接受。

Model – 在业务逻辑代码中操作的业务模型。

Repository
包含接口让外层类实现,如操作数据库的类等。Interactor用这些接口的实现类来读取和存储数据。这也叫资源库模式Repository
Pattern。

Executor – 通过Worker Thread
Executor让Interactor在后台执行。一般不需要修改这个包里的代码。

澳门新葡亰网站注册 3MVP结构(把Controller改为Presenter)

以下是例子

在这个简单例子中,我们的use
case是在应用启动时读取数据库中的欢迎语句并展示。下面演示如何编写代码包让use
case运行起来。

  • presentation包
  • storage包
  • domain包

前两个包属于外层,最后一个包属于内层(核心层)。

presentation包负责将信息展示在屏幕上,而且包含整个MVP栈,即同时包含UI和presenter这两个属于不同层的组件。下面上码。

这里可以参考google在github上的示例。这个工程里面实现了很多架构,每个架构都是单独一个分支,其中分支TODO-MVP就是演示的MVP。下面简单解读一下这个工程:1
工程结构

写一个内层的Interactor

你可以从任何一层开始编写,我建议从内层的逻辑代码写起。因为逻辑代码写好之后可以测试,不需要activity也可以正常运行。

澳门新葡亰网站注册,所以我们先写一个Interactor,这个Interactor包含了处理业务逻辑的代码。**所有的Interactor都应该在后台运行,而不应影响UI展示。**我在这里先编写一个WelcomingInteractor。

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

Callback负责与主线程的UI组件联通。将它放在WelcomingInteractor中可以避免给所有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() {

        // 获取消息
        final String message = mMessageRepository.getWelcomeMessage(); 

        // 检查是否获取失败
        if (message == null || message.length() == 0) {

            // 在主线程中通知错误
            notifyError();

            return;
        }

        // 已成功获取消息,通知UI
        postMessage(message);
    }
}

这段代码获取了数据,并向UI层发送数据或报错。这里通过Callback向UI发送信息,这个Callback扮演的是presenter的角色。这段代码是逻辑的核心,其他代码都是依赖框架的。看一下这个类的引用:

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或数据库,只需要调用外层实现的Callback的回调方法。

澳门新葡亰网站注册 4todomvp工程结构

测试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环境下运行。

  • 按照功能模块划分包本工程按照不同的功能来分包,如todoList,taskdetail,statistic等,每个功能都是相对独立的。通常来讲,我们可以用功能分包或者按照层次(如view,presenter,model等)来分包。关于那种分包模式好,Clean
    Architecture的作者做了一些有意义的论述,其中旗帜鲜明的支持按照功能分包。
  • 功能模块的结构在每个功能模块中,主要包括了该功能的Activity、Fragment、Presenter、Contract四个类文件。Activity用于创建Presenter和View,Fragment实现了View的接口,Presenter实现了各项逻辑,Contract用于把Presenter和View联系在一起。Model在各个功能模块之外,为各模块提供支持。

编写presentation层

Presentation层在Clean架构中属于外层的范围,它依赖于框架,包含了UI展示的代码。我们用MainActivity类在应用启动时展示欢迎信息。

首先编写Presenter和View的接口。View只需要展示欢迎信息。

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

那怎么在App启动时运行Interactor呢?所有和View无关的代码都写进Presenter类中。这样可以实现关注分离(Separation
of
Concerns)并能避免Activity过于复杂。这些代码包括和Interactor交互的代码。

在MainActivity中重写onResume()方法。

@Override
protected void onResume() {
    super.onResume();

    // 在活动resume时开始获取数据
    mPresenter.resume();
}

所有的Presenter在继承BasePresenter时都要实现resume()方法。我们在MainPresenter的onResume()方法中启动Interactor。

@Override
public void resume() {

    mView.showProgress();

    // 初始化Interactor
    WelcomingInteractor interactor = new WelcomingInteractorImpl(
            mExecutor,
            mMainThread,
            this,
            mMessageRepository
    );

    // 执行interactor
    interactor.execute();
}

execute()方法会在后台线程中调用WelcomingInteractorImpl类的run()方法。run()方法的实现可以看上文写一个内层的Interactor部分。

你可能已经发现Interactor很像AsyncTask,都是提供所有需要的东西然后运行。那为什么不用AsyncTask呢?因为AsyncTask是Android的代码,需要模拟器来运行与测试。

在上面的代码中我们给Interactor传入了下列属性:

  • ThreadExecutor对象:用于在后台线程运行Interactor。我喜欢将这个类设计成单例。这个类属于domain包,不需要在外层实现。
  • MainThreadImpl对象:用于在主线程中执行Interactor的Runnable对象。在依赖框架的外层代码中我们可以访问主线程,所以这个类要在外层实现。
  • 我们传入this是因为MainPresenter也是一个Callback对象,Interactor要通过Callback来更新UI。
  • 我们传入实现了MessageRepository接口的WelcomMessageRepository对象让Interactor使用。下面会讲到WelcomMessageRepository。

为什么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 {

View用于展示消息:

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

Presentation层的东西就这么多了。

2 MVP在工程中的使用

编写Storage层

repository中的接口就在storage层实现。所有与数据库相关的代码都在这里。资源库模式下数据的来源是不确定的,意思是逻辑代码不关心数据的来源,不论是数据库、服务器还是文件。

你可以用ContentProvider或DBFlow等ORM工具处理更复杂的数据。如果你需要从网络获取数据那你可以用Retrofit。如果你只需要基本的键值对存储那你可以用SharedPreferences。不管怎样,一定要选对工具。

这里我们的数据库不是真正的数据库,只是一个模拟了延迟的一个很简单的类。

public class WelcomeMessageRepository implements MessageRepository {
    @Override
    public String getWelcomeMessage() {
        String msg = "Welcome, friend!"; 
        // 模拟网络/数据库延迟
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return msg;
    }
}

WelcomingInteractor可能以为延迟是网络或其他原因造成的,但它并不关心,它只需要数据提供者实现了MessageRepository接口。

  • 使用contract把同一个功能的view和presenter联系在一起,如:

总结

详细代码请看这个git
repo。总结一下各个类的触发顺序:

MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity

控制流的顺序:

Outer — Mid — Core — Outer — Core — Mid — Outer

在一个use
case中多次访问外层很正常。比如当你要显示、存储加访问网络,你的控制流会访问外层至少三次。

public interface TasksContract { interface View extends BaseView<Presenter>{} interface Presenter extends BasePresenter{}}
  • Fragment继承View,来实现界面的具体逻辑