一文說透 Android 應用架構 MVC、MVP、MVVM 和 組件化


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 設計模式,我們給出一個示例程序。你可以在 Android-references 中獲取到它的源代碼。

2.2 示例程序

在該示例中,我們使用了:

  1. 開眼視頻的 API 作為數據源;

  2. Retrofit 進行數據訪問;

  3. 使用 ARouter 進行路由;

  4. 使用 MVP 設計模式作為程序架構。

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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3OsWwKEl-1614135815610)(https://upload-images.jianshu.io/upload_images/23587538-2db584291d462157?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “包結構”)]

這里核心的代碼是 MVP 部分。

這里我們首先定義了 MVP 模式中的最頂層的 View 和 Presenter,在這里分別是 BaseView 和 BasePresenter,它們在該項目中是兩個空的接口,在一些項目中,我們可以根據自己的需求在這兩個接口中添加自己需要的方法。

然后,我們定義了 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 的方法。從上面我們也可以看出,這里的 IView 和 IPresenter 分別實現了 BaseView 和 BasePresenter

上面,我們定義了 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 通過 Mode l獲取數據,並在拿到數據的時候,通過 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 的方法來展示數據。

另外一個值得注意的地方就是,在實際的使用過程中,尤其是進行異步請求的時候,為了給 Model 和 View 之間解耦,我們會在 Presenter 中使用 Handler 收發消息來建立兩者之間的橋梁。

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 的實際的應用。你可以在 Android-references 中獲取到它的源代碼。

3.2 示例程序

在該項目中,我們使用了:

  1. 果殼網的 API 作為數據源;

  2. 使用 Retrofit 進行網絡數據訪問;

  3. 使用 ViewMdeol 作為整體的架構設計。

該項目的包結構如下圖所示:

這里的 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 組件化實踐

1. 包結構

首先,我們先來看整個應用的包的結構。如下圖所示,該模塊的划分是根據各個模塊的功能來決定的。圖的右側白色的部分是各個模塊的文件路徑,我推薦使用這種方式,而不是將各個模塊放置在 app 下面,因為這樣看起來更加的清晰。為了達到這個目的,你只需要按照下面的方式在 settings.gralde 里面配置一下各個模塊的路徑即可。注意在實際應用的時候模塊的路徑的關系,不要搞錯了。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zpAi9Ik8-1614135815614)(https://upload-images.jianshu.io/upload_images/23587538-e0f8f970cc8118ed?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “組件化”)]

然后,我們介紹一下這里的 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')
}

 

2. 路由

接着,我們來看一下路由框架的配置。這里,我們使用阿里的 ARouter 來進行頁面之間的跳轉,你可以在 Github 上面了解該框架的配置和使用方式。這里我們只講解一下在組件化開發的時候需要注意的地方。注意到 ARouter 是通過注解來進行頁面配置的,並且它的注解是在編譯的時候進行處理的。所以,我們需要引入 arouter-compiler 來使用它的編譯時處理功能。需要注意的地方是,我們只要在公共的模塊中加入 arouter-api 就可以使用 ARouter 的 API 了,但是需要在每個模塊中引入 arouter-compiler 才能使用編譯時注解。也就是說,我們需要在每個模塊中都加入 arouter-compiler 依賴。

ARouter 的實現原理使用了 Java 中的注解處理,你可以通過閱讀我之前寫的一篇文章來了解 《Java 開發者核心技能之 Java 注解及其典型的使用方法》

3. 模塊獨立

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

本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含了Android組件化最全開源項目(美團App、得到App、支付寶App、微信App、蘑菇街App、有贊APP…)等,資源持續更新中…
高級Android組件化強化實戰

原文作者:ShouHeng [Hello 開發者](javascript:void(0)😉


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM