第四部分:MVVM
前面的主要內容,基本只是介紹了MVVM的基礎DataBinding的語法和使用。但這遠遠不夠,下面纔是本文的重頭戲。Android 的關於代碼的組織方式(你也可以稱之爲設計模式),從MVC 到MVP 再到MVVM,經歷了三次重要變化。
MVVM的前世今生
MVC
Android設計之初,就遵循的MVC模式,此時的模塊劃分是:
- Model:所有和數據相關的類
- View:佈局文件
- Controller:Activity、Fragment
但因爲佈局文件又不能完全承擔自己的職責,我們被迫將一部分更新View的代碼寫在了Activity(也就是Controller)中,導致Controller中的代碼混亂,不利於閱讀和維護,既有Controller的邏輯,也有View層邏輯。
想象一下一個業務場景:在界面上點擊一個Button,登錄一個網站並將登錄結果在界面上呈現。
這個需求,我們需要有一個界面,來放點擊按鈕,和登錄結果的TextView,這就是View層。再新建一個LoginHelper類來處理網絡數據,這就是Model層。剩下一個Controller就是Activity,在Activity中,通過button的OnClickListener
觸發Model層的網絡請求後,將登錄的結果返回給Activity,Activity可以直接將結果顯示到界面的TextView上。
是不是很清晰,各司其職。但當你的需求中,包含需要動態改變界面控件隱藏和顯示、界面顏色變化、動畫控制,你也只能把這些View相關的代碼寫在Controller的Activity中。
造成這種混亂的根本原因,是作爲View層的佈局文件,本身控制能力太弱,View層需要動態改變的東西,不得不依賴Contoller提供控制。
MVP
爲了解決Controller邏輯混亂的問題,於是有了MVP模式。MVP模塊劃分:
- Model:所有和數據相關的類,和MVC中的Model職責一致,處理數據邏輯。
- View:佈局文件 + Activity or Fragment
- Presenter:需要額外增加接口和類
MVC中的作爲View層的xml不是控制力弱嗎?直接把Activity和Fragment劃分給View層,控制力是不是爆表!!!這就是MVP模式。
而Presenter作爲Controller的替代者,接替了它的工作。什麼工作?當然是連接Model和View層。
Presenter通常由兩部分組成:
- 接口:讓View層的Activity或者Fragment實現,用於通知View層更新界面。
- 具體的Presenter類,比如LoginPresenter,它持有Model層和View層的引用。用於處理業務邏輯。
MVP的工作原理是,通過View層(Activity或者Fragment)創建對應的Presenter,並相互持有彼此的引用。在Presenter中,創建Model實例,並獲取Model處理後的數據,通過接口通知View層更新界面。
MVP的一大好處是,通過接口,讓Model和View層解耦,可以實現面向接口編程的目的,這對於大型項目的開發,和各模塊單獨測試幫助很大。
那麼,MVP有什麼缺點呢?
MVP的缺點是,額外增加了一個接口,會增加額外的維護成本。真是的應用場景是,需求不斷變化,接口也需要做對應調整。
該問題的解決方法是,在定義接口時,儘可能的斟酌接口,合理利用繼承、實現,合理的使用設計模式。
MVVM
對於MVVM來說,它的模塊分爲:
- Model:Model還是哪個Model,一直都沒變。
- View:依然是佈局文件 + Activity + Fragment。
- ViewModel:就是被綁定在View層對象所屬的類。
本文的最終目的就是這個,以一個下載代碼的例子說明。只貼核心代碼,Demo源碼見:
Model
public class Model {
public interface OnDownloadListener {
void onDownloadSuccess();
void onDownloading(int progress);
void onDownloadFailed();
}
public static void downloadFile(final String url, final String dir, final OnDownloadListener listener) {
// ...
listener.onDownloading(progress);
// ...
}
}
爲了顯而易見,我將唯一屬於Model模塊的類直接命名爲Model。該類使用Okhttp網絡框架,用於下載文件。
OnDownloadListener
接口用於通知ViewModel下載的結果,所以ViewModel需要實現該接口。public static void downloadFile(final String url, final String dir, final OnDownloadListener listener)
:用於具體的下載邏輯,供外界調用。url
爲需要下載的地址,dir
爲下載文件存放的目錄,listener
爲下載監聽。listener.onDownloading(progress);
:下載過程中,調用監聽器,通知View層更新下載進度。
在該例中,數據只有來自網絡的數據流,而Model類負責下載和存儲數據,作爲Model層存在。
ViewModel
public class ViewModel implements Model.OnDownloadListener {
private static final String TAG = "ViewModel";
private static final String URL = "http://172.0.6.55/test_for_live.mp4";
public ObservableInt mDownloadProgress = new ObservableInt(0);
public void onClick(View view) { // 點擊事件處理函數
Log.i(TAG, "onClick");
Model.downloadFile(URL, "/mvvm/", this); // 連接Model
}
@Override
public void onDownloadSuccess() {
Log.i(TAG, "onDownloadSuccess");
}
@Override
public void onDownloading(int progress) {
mDownloadProgress.set(progress); // 進度更新變量
}
@Override
public void onDownloadFailed() {
Log.i(TAG, "onDownloadFailed");
}
}
本例中,ViewModel類作爲ViewMode層的爲一類,方便起見也起名叫ViewModel,希望不要搞混了。
ViewMode層的主要工作是將View層和Model層連接起來,主要靠如下關鍵調用實現:
-
public void onClick(View view)
:該函數定義後,將會被綁定在View層的Dowload按鈕上,點擊DowLoad按鈕,就會執行Model層的下載邏輯。 -
Model.downloadFile(URL, "/mvvm/", this);
:DownLoad按鈕點擊後,立馬執行下載,由該函數調用可以看到Model是如何與ViewModel連接的。函數調用後,下載進度,將會通過OnDownloadListener
的回調接口通知到ViewModel層。當然,這裏是靜態調用。使用類實例的方式調用應該更加常見一些。
-
mDownloadProgress.set(progress);
:定義了一個ObservableInt對象mDownloadProgress,該對象將和View層綁定,當下載進度更新時,mDownloadProgress值變化將直接觸發View層的刷新。
View
View層的佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ProgressBar
android:id="@+id/pb"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="10dp"
android:max="100"
android:progress="@{viewModel.mDownloadProgress}"
android:progressDrawable="@drawable/progressbar_preview"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/bt_download"
app:layout_constraintWidth_percent="0.875" />
<Button
android:id="@+id/bt_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{viewModel::onClick}"
android:text="DownLoad"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<data class="MvvmBinding">
<variable
name="viewModel"
type="com.superli.demo.ViewModel" />
</data>
</layout>
View層的Activity:
public class MVVMActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MvvmBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_mvvm);
binding.setViewModel(new ViewModel());
}
}
View層的工作主要就是將ViewModel和自己綁定,同時通過ViewMode間接和Model中的數據綁定,這種綁定關係並不直觀,但顯然是存在的。
以上就是MVVM代碼原因各層的劃分方式了。
MVVM的不足
MVC的問題是View層和Controller層在Activity中融合到一起了,原因是xml佈局文件的能力太弱,無法控制一些動態的內容。在MVVM中,data binding通過數據綁定的方式解決了這個問題,使View層和Model可以各司其職。但當你需要處理一些特殊的邏輯時,還是會讓View層出現不必要的代碼。
比如在本例中,我現在想控制ProgressBar
,做一個移動或者隨便什麼樣式的動畫,我將不得不在Activity中做控制,但控制的邏輯原則上應該交給ViewModel啊。類似的情況還有很多。
關於data binding的使用,Google官方探索除了一套MVP + data binding的道路,原理大概是通過data binding解決數據綁定的問題,使用Presenter和Model交互。相關sample源碼見:
有興趣可以去研究一下。
術語表
文中術語 | 含義 |
---|---|
事件 | 即Event |