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各有各自的特色,能夠根據應用開發的須要選擇適合本身的架構模式。組件化的目的就在於保持各個模塊之間的獨立從而便於分工協做。它們之間的關係就是,你能夠在組件化的各個模塊中應用前面三種架構模式的一種或者幾種。

相關文章
相關標籤/搜索