Android 架构设计:MVC、MVP、MVVM和组件化

MVC、MVP和MVVM是常见的三种架构设计模式,当前MVP和MVVM的使用相对比较普遍,固然MVC也并无过期之说。而所谓的组件化就是指将应用根据业务需求划分红各个模块来进行开发,每一个模块又能够编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可使用前面三种架构设计。咱们只有了解了这些架构设计的特色以后,才能在进行开发的时候选择适合本身项目的架构模式,这也是本文的目的。java

一、MVC

MVC (Model-View-Controller, 模型-视图-控制器),标准的MVC是这个样子的:android

  • 模型层 (Model):业务逻辑对应的数据模型,无View无关,而与业务相关;
  • 视图层 (View):通常使用XML或者Java对界面进行描述;
  • 控制层 (Controllor):在Android中一般指Activity和Fragment,或者由其控制的业务类。

Activity并不是标准的Controller,它一方面用来控制了布局,另外一方面还要在Activity中写业务代码,形成了Activity既像View又像Controller。git

在Android开发中,就是指直接使用Activity并在其中写业务逻辑的开发方式。显然,一方面Activity自己就是一个视图,另外一方面又要负责处理业务逻辑,所以逻辑会比较混乱。github

这种开发方式不太适合Android开发。设计模式

二、MVP

2.1 概念梳理

MVP (Model-View-Presenter) 是MVC的演化版本,几个主要部分以下:api

  • 模型层 (Model):主要提供数据存取功能。
  • 视图层 (View):处理用户事件和视图。在Android中,多是指Activity、Fragment或者View。
  • 展现层 (Presenter):负责经过Model存取书数据,链接View和Model,从Model中取出数据交给View。

因此,对于MVP的架构设计,咱们有如下几点须要说明:bash

  1. 这里的Model是用来存取数据的,也就是用来从指定的数据源中获取数据,不要将其理解成MVC中的Model。在MVC中Model是数据模型,在MVP中,咱们用Bean来表示数据模型。
  2. Model和View不会直接发生关系,它们须要经过Presenter来进行交互。在实际的开发中,咱们能够用接口来定义一些规范,而后让咱们的View和Model实现它们,并借助Presenter进行交互便可。

为了说明MVP设计模式,咱们给出一个示例程序。你能够在Github中获取到它的源代码。网络

2.2 示例程序

在该示例中,咱们使用了:架构

  1. 开眼视频的API做为数据源;
  2. Retrofit进行数据访问;
  3. 使用ARouter进行路由;
  4. 使用MVP设计模式做为程序架构。

下面是该模块的基本的包结构:app

包结构

这里核心的代码是MVP部分。

这里咱们首先定义了MVP模式中的最顶层的View和Presenter,在这里分别是BaseViewBasePresenter,它们在该项目中是两个空的接口,在一些项目中,咱们能够根据本身的需求在这两个接口中添加本身须要的方法。

而后,咱们定义了HomeContract。它是一个抽象的接口,至关于一层协议,用来规定指定的功能的View和Presenter分别应该具备哪些方法。一般,对于不一样的功能,咱们须要分别实现一个MVP,每一个MVP都会又一个对应的Contract。笔者认为它的好处在于,将指定的View和Presenter的接口定义在一个接口中,更加集中。它们各自须要实现的方法也一目了然地展示在了咱们面前。

这里根据咱们的业务场景,该接口的定义以下:

public interface HomeContract {

        interface IView extends BaseView {
            void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists);
            void setNextPage(List<HomeBean.IssueList.ItemList> itemLists);
            void onError(String msg);
        }

        interface IPresenter extends BasePresenter {
            void requestFirstPage();
            void requestNextPage();
        }
    }
复制代码

HomeContract用来规定View和Presenter应该具备的操做,在这里它用来指定主页的View和Presenter的方法。从上面咱们也能够看出,这里的IViewIPresenter分别实现了BaseViewBasePresenter

上面,咱们定义了V和P的规范,MVP中还有一项Model,它用来从网络中获取数据。这里咱们省去网络相关的具体的代码,你只须要知道APIRetrofit.getEyepetizerService()是用来获取Retrofit对应的Service,而getMoreHomeData()getFirstHomeData()是用来从指定的接口中获取数据就行。下面是HomeModel的定义:

public class HomeModel {

    public Observable<HomeBean> getFirstHomeData() {
        return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());
    }

    public Observable<HomeBean> getMoreHomeData(String url) {
        return APIRetrofit.getEyepetizerService().getMoreHomeData(url);
    }
}

复制代码

OK,上面咱们已经完成了Model的定义和View及Presenter的规范的定义。下面,咱们就须要具体去实现View和Presenter。

首先是Presenter,下面是咱们的HomePresenter的定义。在下面的代码中,为了更加清晰地展现其中的逻辑,我删减了一部分无关代码:

public class HomePresenter implements HomeContract.IPresenter {

    private HomeContract.IView view;

    private HomeModel homeModel;

    private String nextPageUrl;

    // 传入View并实例化Model
    public HomePresenter(HomeContract.IView view) {
        this.view = view;
        homeModel = new HomeModel();
    }

    // 使用Model请求数据,并在获得请求结果的时候调用View的方法进行回调
    @Override
    public void requestFirstPage() {
        Disposable disposable = homeModel.getFirstHomeData()
                // ....
                .subscribe(itemLists -> { view.setFirstPage(itemLists); },
                        throwable -> { view.onError(throwable.toString()); });
    }

    // 使用Model请求数据,并在获得请求结果的时候调用View的方法进行回调
    @Override
    public void requestNextPage() {
        Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
                // ....
                .subscribe(itemLists -> { view.setFirstPage(itemLists); },
                        throwable -> { view.onError(throwable.toString()); });
    }
}
复制代码

从上面咱们能够看出,在Presenter须要将View和Model创建联系。咱们须要在初始化的时候传入View,并实例化一个Model。Presenter经过Model获取数据,并在拿到数据的时候,经过View的方法通知给View层。

而后,就是咱们的View层的代码,一样,我对代码作了删减:

@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity<ActivityEyepetizerMenuBinding> implements HomeContract.IView {

    // 实例化Presenter
    private HomeContract.IPresenter presenter;
    {
        presenter = new HomePresenter(this);
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.activity_eyepetizer_menu;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        // ...
        // 使用Presenter请求数据
        presenter.requestFirstPage();
        loading = true;
    }

    private void configList() {
        // ...
        getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    // 请求下一页的数据
                    presenter.requestNextPage();
                }
            }
        });
    }

    // 当请求到结果的时候在页面上作处理,展现到页面上
    @Override
    public void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists) {
        loading = false;
        homeAdapter.addData(itemLists);
    }

    // 当请求到结果的时候在页面上作处理,展现到页面上
    @Override
    public void setNextPage(List<HomeBean.IssueList.ItemList> itemLists) {
        loading = false;
        homeAdapter.addData(itemLists);
    }

    @Override
    public void onError(String msg) {
        ToastUtils.makeToast(msg);
    }

    // ...
}
复制代码

从上面的代码中咱们能够看出实际在View中也要维护一个Presenter的实例。 当须要请求数据的时候会使用该实例的方法来请求数据,因此,在开发的时候,咱们须要根据请求数据的状况,在Presenter中定义接口方法。

实际上,MVP的原理就是View经过Presenter获取数据,获取到数据以后再回调View的方法来展现数据。

2.3 MVC 和 MVP 的区别

  1. MVC 中是容许 Model 和 View 进行交互的,而MVP中,Model 与 View 之间的交互由Presenter完成;
  2. MVP 模式就是将 P 定义成一个接口,而后在每一个触发的事件中调用接口的方法来处理,也就是将逻辑放进了 P 中,须要执行某些操做的时候调用 P 的方法就好了。

2.4 MVP的优缺点

优势:

  1. 下降耦合度,实现了 Model 和 View 真正的彻底分离,能够修改 View 而不影响 Modle;
  2. 模块职责划分明显,层次清晰;
  3. 隐藏数据;
  4. Presenter 能够复用,一个 Presenter 能够用于多个 View,而不须要更改 Presenter 的逻辑;
  5. 利于测试驱动开发,之前的Android开发是难以进行单元测试的;
  6. View 能够进行组件化,在MVP当中,View 不依赖 Model。

缺点:

  1. Presenter 中除了应用逻辑之外,还有大量的 View->Model,Model->View 的手动同步逻辑,形成 Presenter 比较笨重,维护起来会比较困难;
  2. 因为对视图的渲染放在了 Presenter 中,因此视图和 Presenter 的交互会过于频繁;
  3. 若是 Presenter 过多地渲染了视图,每每会使得它与特定的视图的联系过于紧密,一旦视图须要变动,那么Presenter也须要变动了。

三、MVVM (分手大师)

3.1 基础概念

MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让咱们将视图 UI 和业务逻辑分开。

  • 模型层 (Model):负责从各类数据源中获取数据;
  • 视图层 (View):在 Android 中对应于 Activity 和 Fragment,用于展现给用户和处理用户交互,会驱动 ViewModel 从 Model 中获取数据;
  • ViewModel 层:用于将 Model 和 View 进行关联,咱们能够在 View 中经过 ViewModel 从 Model 中获取数据;当获取到了数据以后,会经过自动绑定,好比 DataBinding,来将结果自动刷新到界面上。

使用 Google 官方的 Android Architecture Components ,咱们能够很容易地将 MVVM 应用到咱们的应用中。下面,咱们就使用它来展现一下 MVVM 的实际的应用。你能够在Github中获取到它的源代码。

3.2 示例程序

在该项目中,咱们使用了:

  1. 果壳网的 API 做为数据源;
  2. 使用 Retrofit 进行网络数据访问;
  3. 使用 ViewMdeol 做为总体的架构设计。

该项目的包结构以下图所示:

mvvm

这里的model.data下面的类是对应于网络的数据实体的,由JSON自动生成,这里咱们不进行详细描述。这里的model.repository下面的两个类是用来从网络中获取数据信息的,咱们也忽略它的定义。

上面就是咱们的 Model 的定义,并无太多的内容,基本与 MVP 一致。

下面的是 ViewModel 的代码,咱们选择了其中的一个方法来进行说明。当咱们定义 ViewModel 的时候,须要继承 ViewModel 类。

public class GuokrViewModel extends ViewModel {

    public LiveData<Resource<GuokrNews>> getGuokrNews(int offset, int limit) {
        MutableLiveData<Resource<GuokrNews>> result = new MutableLiveData<>();
        GuokrRetrofit.getGuokrService().getNews(offset, limit)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<GuokrNews>() {
                    @Override
                    public void onError(Throwable e) {
                        result.setValue(Resource.error(e.getMessage(), null));
                    }

                    @Override
                    public void onComplete() { }

                    @Override
                    public void onSubscribe(Disposable d) { }

                    @Override
                    public void onNext(GuokrNews guokrNews) {
                        result.setValue(Resource.success(guokrNews));
                    }
                });
        return result;
    }
}
复制代码

这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,因此,为了使用它,咱们还须要加入下面的依赖:

api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"
复制代码

在 ViewModel 的定义中,咱们直接使用 Retrofit 来从网络中获取数据。而后当获取到数据的时候,咱们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,咱们只须要调用该方法,并对返回的 LiveData 进行"监听"便可。这里,咱们将错误信息和返回的数据信息进行了封装,而且封装了一个表明当前状态的枚举信息,你能够参考源代码来详细了解下这些内容。

上面咱们定义完了 Model 和 ViewModel,下面咱们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。

@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment<FragmentNewsListBinding> {

    private GuokrViewModel guokrViewModel;

    private int offset = 0;

    private final int limit = 20;

    private GuokrNewsAdapter adapter;

    @Override
    protected int getLayoutResId() {
        return R.layout.fragment_news_list;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        // ...

        guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);

        fetchNews();
    }

    private void fetchNews() {
        guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
            if (guokrNewsResource == null) {
                return;
            }
            switch (guokrNewsResource.status) {
                case FAILED:
                    ToastUtils.makeToast(guokrNewsResource.message);
                    break;
                case SUCCESS:
                    adapter.addData(guokrNewsResource.data.getResult());
                    adapter.notifyDataSetChanged();
                    break;
            }
        });
    }
}
复制代码

以上就是咱们的 View 层的定义,这里咱们先使用了

这里的view.fragment包下面的类对应于实际的页面,这里咱们 ViewModelProviders 的方法来获取咱们须要使用的 ViewModel,而后,咱们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”便可。

以上就是 MVVM 的基本使用,固然,这里咱们并无使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。

3.3 MVVM 的优势和缺点

MVVM模式和MVC模式同样,主要目的是分离视图(View)和模型(Model),有几大优势:

  1. 低耦合:视图(View)能够独立于Model变化和修改,一个 ViewModel 能够绑定到不一样的 View 上,当 View 变化的时候 Model 能够不变,当 Model 变化的时候 View 也能够不变。
  2. 可重用性:你能够把一些视图逻辑放在一个 ViewModel 里面,让不少 view 重用这段视图逻辑。
  3. 独立开发:开发人员能够专一于业务逻辑和数据的开发(ViewModel),设计人员能够专一于页面设计。
  4. 可测试:界面素来是比较难于测试的,而如今测试能够针对 ViewModel 来写。

四、组件化

4.1 基础概念

所谓的组件化,通俗理解就是将一个工程分红各个模块,各个模块之间相互解耦,能够独立开发并编译成一个独立的 APP 进行调试,而后又能够将各个模块组合起来总体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协做、同步开发;被分割出来的模块又能够在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤为适用于比较大型的项目。

简单了解了组件化以后,让咱们来看一下如何实现组件化开发。你可能以前据说过组件化开发,或者被其高大上的称谓吓到了,但它实际应用起来并不复杂,至少借助了现成的框架以后并不复杂。这里咱们先梳理一下,在应用组件化的时候须要解决哪些问题:

  1. 如何分红各个模块? 咱们能够根据业务来进行拆分,对于比较大的功能模块能够做为应用的一个模块来使用,可是也应该注意,划分出来的模块不要过多,不然可能会下降编译的速度而且增长维护的难度。
  2. 各个模块之间如何进行数据共享和数据通讯? 咱们能够把须要共享的数据划分红一个单独的模块来放置公共数据。各个模块之间的数据通讯,咱们可使用阿里的 ARouter 进行页面的跳转,使用封装以后的 RxJava 做为 EventBus 进行全局的数据通讯。
  3. 如何将各个模块打包成一个独立的 APP 进行调试? 首先这个要创建在2的基础上,而后,咱们能够在各个模块的 gradle 文件里面配置须要加载的 AndroidManifest.xml 文件,并能够为每一个应用配置一个独立的 Application 和启动类。
  4. 如何防止资源名冲突问题? 遵照命名规约就能规避资源名冲突问题。
  5. 如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题? 能够将各个模块公用的依赖的版本配置到 settings.gradle 里面,而且能够创建一个公共的模块来配置所须要的各类依赖。

Talk is cheap,下面让咱们动手实践来应用组件化进行开发。你能够在Github中获取到它的源代码。

4.2 组件化实践

包结构

首先,咱们先来看整个应用的包的结构。以下图所示,该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径,我推荐使用这种方式,而不是将各个模块放置在 app 下面,由于这样看起来更加的清晰。为了达到这个目的,你只须要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径便可。注意在实际应用的时候模块的路径的关系,不要搞错了。

组件化

而后,咱们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖,这里咱们将二者放在了一个模块中以减小模块的数量。下面是它的 gradle 的部分配置。这里咱们使用了 api 来引入各个依赖,以便在其余的模块中也能使用这些依赖。

dependencies {
    api fileTree(include: ['*.jar'], dir: 'libs')
    // ...
    // router
    api 'com.alibaba:arouter-api:1.3.1'
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    // walle
    api 'com.meituan.android.walle:library:1.1.6'
    // umeng
    api 'com.umeng.sdk:common:1.5.3'
    api 'com.umeng.sdk:analytics:7.5.3'
    api files('libs/pldroid-player-1.5.0.jar')
}
复制代码

路由

接着,咱们来看一下路由框架的配置。这里,咱们使用阿里的 ARouter 来进行页面之间的跳转,你能够在Github上面了解该框架的配置和使用方式。这里咱们只讲解一下在组件化开发的时候须要注意的地方。注意到 ARouter 是经过注解来进行页面配置的,而且它的注解是在编译的时候进行处理的。因此,咱们须要引入arouter-compiler来使用它的编译时处理功能。须要注意的地方是,咱们只要在公共的模块中加入arouter-api就可使用ARouter的API了,可是须要在每一个模块中引入arouter-compiler才能使用编译时注解。也就是说,咱们须要在每一个模块中都加入arouter-compiler依赖。

模块独立

为了可以将各个模块编译成一个独立的 APP,咱们须要在 Gradle 里面作一些配置。

首先,咱们须要在gradle.properties定义一些布尔类型的变量用来判断各个模块是做为一个 library 仍是 application 进行编译。这里个人配置以下面的代码所示。也就是,我为每一个模块都定义了这么一个布尔类型的变量,固然,你也能够只定义一个变量,而后在各个模块中使用同一个变量来进行判断。

isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false
复制代码

而后,咱们来看一下各个模块中的 gradle 该如何配置,这里咱们以开眼视频的功能模块做为例子来进行讲解。首先,一个模块做为 library 仍是 application 是根据引用的 plugin 来决定的,因此,咱们要根据以前定义的布尔变量来决定使用的 plugin:

if (isEyepetizerModuleApp.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
复制代码

假如咱们要将某个模块做为一个独立的 APP,那么启动类你确定须要配置。这就意味着你须要两个 AndroidManifest.xml 文件,一个用于 library 状态,一个用于 application 状态。因此,咱们能够在 main 目录下面再定义一个 AndroidManifest.xml,而后,咱们在该配置文件中不仅指定启动类,还使用咱们定义的 Application。指定 Application 有时候是必须的,好比你须要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了,能够参考源码,这里咱们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。

以下所示,咱们能够根据以前定义的布尔值来决定使用哪个配置文件:

sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isEyepetizerModuleApp.toBoolean()) {
                manifest.srcFile "src/main/debug/AndroidManifest.xml"
            } else {
                manifest.srcFile "src/main/AndroidManifest.xml"
            }
        }
    }
复制代码

此外,还须要注意的是,若是咱们但愿在每一个模块中都能应用 DataBinding 和 Java 8 的一些特性,那么你须要在每一个模块里面都加入下面的配置:

// use data binding
    dataBinding {
        enabled = true
    }
    // use java 8 language
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
复制代码

对于编译时注解之类的配置,咱们也须要在每一个模块里面都进行声明。

完成了以上的配置,咱们只要根据须要编译的类型,修改以前定义的布尔值,来决定是将该模块编译成 APP 仍是做为类库来使用便可。

以上就是组件化在 Android 开发当中的应用。

总结

MVC、MVP和MVVM各有各自的特色,能够根据应用开发的须要选择适合本身的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协做。它们之间的关系就是,你能够在组件化的各个模块中应用前面三种架构模式的一种或者几种。

相关文章
相关标签/搜索