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的架構設計,我們有以下幾點需要說明:
- 這裡的Model是用來存取資料的,也就是用來從指定的資料源中擷取資料,不要将其了解成MVC中的Model。在MVC中Model是資料模型,在MVP中,我們用Bean來表示資料模型。
- Model和View不會直接發生關系,它們需要通過Presenter來進行互動。在實際的開發中,我們可以用接口來定義一些規範,然後讓我們的View和Model實作它們,并借助Presenter進行互動即可。
為了說明MVP設計模式,我們給出一個示例程式。你可以在Github中擷取到它的源代碼。
2.2 示例程式
在該示例中,我們使用了:
- 開眼視訊的API作為資料源;
- Retrofit進行資料通路;
- 使用ARouter進行路由;
- 使用MVP設計模式作為程式架構。
下面是該子產品的基本的包結構:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLigTM3cjN9MnJn5Gc9YmJxEDO9gmJ2AjMx0zd-YTMwUzMyEzMhFmYjVTN2EzLcFjMvwFOvwFOxAjMvw1bp5Sd0lGeu4GZj1CZs92ZtIXZzV3Lc9CX6MHc0RHaiojIsJye.jpg)
這裡核心的代碼是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通過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 的差別
- MVC 中是允許 Model 和 View 進行互動的,而MVP中,Model 與 View 之間的互動由Presenter完成;
- MVP 模式就是将 P 定義成一個接口,然後在每個觸發的事件中調用接口的方法來處理,也就是将邏輯放進了 P 中,需要執行某些操作的時候調用 P 的方法就行了。
2.4 MVP的優缺點
優點:
- 降低耦合度,實作了 Model 和 View 真正的完全分離,可以修改 View 而不影響 Modle;
- 子產品職責劃分明顯,層次清晰;
- 隐藏資料;
- Presenter 可以複用,一個 Presenter 可以用于多個 View,而不需要更改 Presenter 的邏輯;
- 利于測試驅動開發,以前的Android開發是難以進行單元測試的;
- View 可以進行元件化,在MVP當中,View 不依賴 Model。
缺點:
- Presenter 中除了應用邏輯以外,還有大量的 View->Model,Model->View 的手動同步邏輯,造成 Presenter 比較笨重,維護起來會比較困難;
- 由于對視圖的渲染放在了 Presenter 中,是以視圖和 Presenter 的互動會過于頻繁;
- 如果 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 示例程式
在該項目中,我們使用了:
- 果殼網的 API 作為資料源;
- 使用 Retrofit 進行網絡資料通路;
- 使用 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),有幾大優點:
- 低耦合:視圖(View)可以獨立于Model變化和修改,一個 ViewModel 可以綁定到不同的 View 上,當 View 變化的時候 Model 可以不變,當 Model 變化的時候 View 也可以不變。
- 可重用性:你可以把一些視圖邏輯放在一個 ViewModel 裡面,讓很多 view 重用這段視圖邏輯。
- 獨立開發:開發人員可以專注于業務邏輯和資料的開發(ViewModel),設計人員可以專注于頁面設計。
- 可測試:界面素來是比較難于測試的,而現在測試可以針對 ViewModel 來寫。
4、元件化
4.1 基礎概念
所謂的元件化,通俗了解就是将一個工程分成各個子產品,各個子產品之間互相解耦,可以獨立開發并編譯成一個獨立的 APP 進行調試,然後又可以将各個子產品組合起來整體構成一個完整的 APP。它的好處是當工程比較大的時候,便于各個開發者之間分工協作、同步開發;被分割出來的子產品又可以在項目之間共享,進而達到複用的目的。元件化有諸多好處,尤其适用于比較大型的項目。
簡單了解了元件化之後,讓我們來看一下如何實作元件化開發。你可能之前聽說過元件化開發,或者被其高大上的稱謂吓到了,但它實際應用起來并不複雜,至少借助了現成的架構之後并不複雜。這裡我們先梳理一下,在應用元件化的時候需要解決哪些問題:
- 如何分成各個子產品? 我們可以根據業務來進行拆分,對于比較大的功能子產品可以作為應用的一個子產品來使用,但是也應該注意,劃分出來的子產品不要過多,否則可能會降低編譯的速度并且增加維護的難度。
- 各個子產品之間如何進行資料共享和資料通信? 我們可以把需要共享的資料劃分成一個單獨的子產品來放置公共資料。各個子產品之間的資料通信,我們可以使用阿裡的 ARouter 進行頁面的跳轉,使用封裝之後的 RxJava 作為 EventBus 進行全局的資料通信。
- 如何将各個子產品打包成一個獨立的 APP 進行調試? 首先這個要建立在2的基礎上,然後,我們可以在各個子產品的 gradle 檔案裡面配置需要加載的 AndroidManifest.xml 檔案,并可以為每個應用配置一個獨立的 Application 和啟動類。
- 如何防止資源名沖突問題? 遵守命名規約就能規避資源名沖突問題。
- 如何解決 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各有各自的特點,可以根據應用開發的需要選擇适合自己的架構模式。元件化的目的就在于保持各個子產品之間的獨立進而便于分工協作。它們之間的關系就是,你可以在元件化的各個子產品中應用前面三種架構模式的一種或者幾種。
我是 WngShhng. 如果您喜歡我的文章,可以在以下平台關注我:
- 個人首頁:https://shouheng88.github.io/
- 掘金:https://juejin.im/user/585555e11b69e6006c907a2a
- Github:https://github.com/Shouheng88
- CSDN:https://blog.csdn.net/github_35186068
- 微網誌:https://weibo.com/u/5401152113