Android 架構設計:MVC、MVP、MVVM和元件化

WngShhng發表於2019-03-04

MVC、MVP和MVVM是常見的三種架構設計模式,當前MVP和MVVM的使用相對比較廣泛,當然MVC也並沒有過時之說。而所謂的元件化就是指將應用根據業務需求劃分成各個模組來進行開發,每個模組又可以編譯成獨立的APP進行開發。理論上講,元件化和前面三種架構設計不是一個層次的。它們之間的關係是,元件化的各個元件可以使用前面三種架構設計。我們只有瞭解了這些架構設計的特點之後,才能在進行開發的時候選擇適合自己專案的架構模式,這也是本文的目的。

1、MVC

MVC (Model-View-Controller, 模型-檢視-控制器),標準的MVC是這個樣子的:

  • 模型層 (Model):業務邏輯對應的資料模型,無View無關,而與業務相關;
  • 檢視層 (View):一般使用XML或者Java對介面進行描述;
  • 控制層 (Controllor):在Android中通常指Activity和Fragment,或者由其控制的業務類。

Activity並非標準的Controller,它一方面用來控制了佈局,另一方面還要在Activity中寫業務程式碼,造成了Activity既像View又像Controller。

在Android開發中,就是指直接使用Activity並在其中寫業務邏輯的開發方式。顯然,一方面Activity本身就是一個檢視,另一方面又要負責處理業務邏輯,因此邏輯會比較混亂。

這種開發方式不太適合Android開發。

2、MVP

2.1 概念梳理

MVP (Model-View-Presenter) 是MVC的演化版本,幾個主要部分如下:

  • 模型層 (Model):主要提供資料存取功能。
  • 檢視層 (View):處理使用者事件和檢視。在Android中,可能是指Activity、Fragment或者View。
  • 展示層 (Presenter):負責通過Model存取書資料,連線View和Model,從Model中取出資料交給View。

所以,對於MVP的架構設計,我們有以下幾點需要說明:

  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設計模式作為程式架構。

下面是該模組的基本的包結構:

包結構

這裡核心的程式碼是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也需要變更了。

3、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、元件化

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

相關文章