從MVC到MVP,再到MVVM,無論哪種架構,目地都是為了將代碼分到不同的類文件中,讓應用程序的代碼架構變得更清晰,更容易維護。
本文將對MVC,MVP,以及MVVM分別進行詳細的介紹和對比,並放上倉庫地址(同一個項目,分別用3種架構來實現),便於大家理解和掌握。
項目倉庫地址:
MVC MVP MVVM
演示:
大部分Android工程師開始寫代碼的時候,都會將業務邏輯和視圖交互都放在Activity/Fragment中。也就是說Activity/Fragment實際上承擔了Controller和View的功能。 View層做的工作非常少,通常只是作為佈局文件展示一下界面。下面我們通過代碼示例來講解MVC架構在Android的運用。
activity_main.xml文件,純界面展示。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width_="match_parent" android:layout_height="match_parent" android:paddingLeft="24dp" android:paddingRight="24dp" tools:context=".controller.MainActivity">
<TextView android:id="@+id/tvTemperatureAndWindLevel" android:layout_width_="wrap_content" android:layout_height="wrap_content" android:textSize="28sp" android:layout_centerInParent="true"/>
<SeekBar android:id="@+id/sbTemperature" android:layout_width_="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_below="@+id/tvTemperatureAndWindLevel" android:min="20" android:max="30"/>
<Button android:id="@+id/btnChangeWindLevel" android:layout_width_="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_below="@+id/sbTemperature" android:text="設置風力"/>
</RelativeLayout>
AirConditioner.java文件,將空調對象抽象封裝成一個類,並實現簡單的內部邏輯。
public class AirConditioner { //空調溫度 private int temperature; //風力級別 private String windLevel;
public static final String WIND_STRONG = "較強"; public static final String WIND_MIDDLE = "中等"; public static final String WIND_GENTLE = "舒適";
public AirConditioner() { reset(); }
public void changeTemperature(int temperature) { this.temperature = temperature; }
/** * 重置 * */ public void reset() { temperature = 20; windLevel = WIND_MIDDLE; }
/** * 當前溫度指標 * */ public String getCurrentCondition() { return "溫度:" + temperature + "度 | 風力:" + windLevel; }
/** * 修改風力 * */ public void changeWindLevel() { if(windLevel.equals(WIND_STRONG)) { windLevel = WIND_MIDDLE; } else if(windLevel.equals(WIND_MIDDLE)) { windLevel = WIND_GENTLE; } else if(windLevel.equals(WIND_GENTLE)) { windLevel = WIND_STRONG; } } }
MainActivity.java文件,在MainActivity中實例化View和Model。
/** * Controller * */ public class MainActivity extends AppCompatActivity { //View private TextView tvTemperatureAndWindLevel; private SeekBar sbTemperature; private Button btnChangeWindLevel;
//Model private AirConditioner airConditioner;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iniComponent(); }
private void iniComponent() { //Model的初始化 airConditioner = new AirConditioner();
//View的初始化 tvTemperatureAndWindLevel = findViewById(R.id.tvTemperatureAndWindLevel); sbTemperature = findViewById(R.id.sbTemperature); btnChangeWindLevel = findViewById(R.id.btnChangeWindLevel);
sbTemperature.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //用戶通過與View的交互,修改了溫度,這個事件被Controller(Activity)接收到 //Controller對Model進行了修改 //修改完Model後,Controller再對View進行更新 changeTemperature(progress); updateUI(); }
@Override public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override public void onStopTrackingTouch(SeekBar seekBar) {
} });
btnChangeWindLevel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //用戶通過與View的交互,修改了風力,這個事件被Controller(Activity)接收到 //Controller對Model進行了修改 //修改完Model後,Controller再對View進行更新 changeWindLevel(); updateUI(); } });
updateUI(); }
/** * 在Controller(Activity)中對Model進行設置 * */ private void changeTemperature(int temperature) { airConditioner.changeTemperature(temperature); }
/** * 在Controller(Activity)中對Model進行設置 * */ private void changeWindLevel() { airConditioner.changeWindLevel(); }
/** * 在Controller(Activity)中對View進行設置 * */ private void updateUI() { tvTemperatureAndWindLevel.setText(airConditioner.getCurrentCondition()); } } MVC
用戶與View進行交互,相關事件通知給Controller(Activity),Controller接收到後,進而對Model進行交互,交互完成後,Controller再對View進行更新。所有這些操作都在Controller中完成。
當頁面簡單,代碼量不大的情況下,這樣做邏輯簡單清晰,並沒有什麼問題。當頁面和功能複雜的時候,Controller的代碼可能會變得複雜和難以維護。更重要的是,這樣的分層並不清晰,MainActivtiy承擔了部分View的工作,也就是Controller和View沒有徹底解耦。Controller中直接持有View和Model並不是一種好的架構方式,因為View和Model在同一個類中,他們之間有可能繞過Controller直接通信,最終導致項目邏輯混亂。層和層之間最好通過消息或者回調的方式來進行通信。
前面我們提到了,Controller(Activity)直接持有View和Model的架構不是最好的。所以,MVP架構引入了Presenter層,通過Presenter持有View的引用和Model。View的引用通過介面的方式,從Presenter的構造器傳入,對View的更新操作通過介面方法進行,而不能直接對View進行操作,這樣就避免了View和Model的直接通信,將View和Model徹底解耦。Presenter負責View和Model之間的通信,起到橋樑的作用。
注意:此時Model(AirConditioner)和View(activity_main.xml)都沒有變化
1.Presenter介面定義
/** * 為了能夠在Activity生命週期發生變化的時候,收到相應的回調 * 我們通常需要定義這些相應的介面 * */ public interface IPresenter { void onCreate(); void onPause(); void onResume(); void onDestroy(); }
2.Presenter具體實現
public class MainPresenter implements IPresenter { //View(Activity) private MainView mainView;
//Model private AirConditioner airConditionerModel;
public MainPresenter(MainView mainView) { this.mainView = mainView; this.airConditionerModel = new AirConditioner(); }
@Override public void onCreate() { this.airConditionerModel = new AirConditioner(); updateUI(); }
@Override public void onPause() {
@Override public void onResume() {
@Override public void onDestroy() {
/** * 溫度發生變化時候調用 * */ public void onTemperatureChanged(int temperature) { airConditionerModel.changeTemperature(temperature); updateUI(); }
/** * 風力發生變化時候調用 * */ public void onWindLevelChanged() { airConditionerModel.changeWindLevel(); updateUI(); }
/** * 更新UI時調用 * */ public void updateUI() { mainView.setTextViewText(airConditionerModel.getCurrentCondition()); } }
1.定義介面
/** * 將View層所需要的方法,以介面的形式定義好 * */ public interface MainView { void setTextViewText(String text); }
2.實現介面
public class MainActivity extends AppCompatActivity implements MainView//實現介面 {
private TextView tvTemperatureAndWindLevel;
private MainPresenter mainPresenter = new MainPresenter(this);
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iniComponent(); //在生命週期變化時,通知Presenter mainPresenter.onCreate(); }
private void iniComponent() { tvTemperatureAndWindLevel = findViewById(R.id.tvTemperatureAndWindLevel); SeekBar sbTemperature = findViewById(R.id.sbTemperature); Button btnChangeWindLevel = findViewById(R.id.btnChangeWindLevel);
sbTemperature.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //將業務邏輯交給Presenter來做 mainPresenter.onTemperatureChanged(progress); }
btnChangeWindLevel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //將業務邏輯交給Presenter來做 mainPresenter.onWindLevelChanged(); } }); }
/** * Presenter中完成具體的業務(修改了Model層後),回調該方法,修改View * */ @Override public void setTextViewText(String text) { tvTemperatureAndWindLevel.setText(text); }
@Override protected void onPause() { super.onPause(); //在生命週期變化時,通知Presenter mainPresenter.onPause(); }
@Override protected void onResume() { super.onResume(); //在生命週期變化時,通知Presenter mainPresenter.onResume(); }
@Override protected void onDestroy() { super.onDestroy(); //在生命週期變化時,通知Presenter mainPresenter.onDestroy(); } }
可以看到MainActivity實現了MainView介面,並通過下方代碼傳遞到Presenter中,Presenter並不直接持有View,也是持有View的引用。
當Presenter完成對Model的操作後,通過View的引用,對回調方法進行調用,便可以完成View的更新操作。
/** * Presenter中完成具體的業務(修改了Model層後),回調該方法,修改View * */ @Override public void setTextViewText(String text) { tvTemperatureAndWindLevel.setText(text); } MVP
當Presenter完成操作後,並不直接更新View,也是通過View的引用,通過回調的方式,讓View自己更新。這樣便實現了View和Model之間的徹底解耦,也解決了MVC架構中View和Controller之間的耦合問題。但是代碼量也變大了,如果UI效果複雜,那麼很可能需要在View的介面中定義大量的介面方法,項目也會變得非常複雜。並且Presenter和MainActivity變成了相互持有的關係。
為了讓Presenter和MainActivity之間能夠進一步解耦,並且讓View層能承擔更多一些的工作,引入了MVVM架構。由於我們需要應用DataBinding特性,所以要在app的build.gradle中啟用數據綁定。
//啟用dataBinding dataBinding{ enabled=true }
注意:Model(AirConditioner)依然沒有變化
但是佈局文件activity_main.xml變化了。引入了<data/>標籤,並且為佈局文件指定了一個ViewModel。接著,通過@{viewModel.xxx}的方式,便可以讓自動讓視圖控制項和ViewModel中的欄位或者方法綁定在一起,這樣,View在被用戶操作的時候,就能觸發相應的回調。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- 通過viewModel這個自定義的名字,將佈局和MainViewModel關聯起來 --> <data>
<import type="android.view.View"/>
<variable name="viewModel" type="com.michael.mvvmdemo.viewmodel.MainViewModel"/> </data>
<!-- 原視圖的根佈局 --> <RelativeLayout android:layout_width_="match_parent" android:layout_height="match_parent" android:paddingLeft="24dp" android:paddingRight="24dp" tools:context=".view.MainActivity">
<TextView android:id="@+id/tvTemperatureAndWindLevel" android:layout_width_="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textSize="28sp" android:text="@{viewModel.temperatureAndWind}"/>
<SeekBar android:id="@+id/sbTemperature" android:layout_width_="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/tvTemperatureAndWindLevel" android:layout_marginTop="20dp" android:max="30" android:min="20" android:onProgressChanged="@{viewModel.onTemperatureChanged}"/>
<Button android:id="@+id/btnChangeWindLevel" android:layout_width_="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/sbTemperature" android:layout_marginTop="20dp" android:text="設置風力" android:onClick="@{() -> viewModel.onWindLevelChanged()}"/>
</RelativeLayout> </layout>
2.定義ViewModel介面
該介面的作用與MVP架構中的IPresenter介面是一樣的,為了能夠收到MainActivity生命週期的回調。
public interface ViewModel { void onCreate(); void onPause(); void onResume(); void onDestroy(); }
3.實現ViewModel介面
佈局文件activity_main.xml中<data/>標籤所指定的MainViewModel類。
public class MainViewModel implements ViewModel { //Model private AirConditioner airConditionerModel;
//指定可觀察欄位,這樣在佈局文件中就可以引用到該欄位,並實時監聽到該欄位的變化 public final ObservableField<String> temperatureAndWind = new ObservableField<>();
public MainViewModel() { this.airConditionerModel = new AirConditioner(); }
/** * 溫度發生變化時候調用 * */ public void onTemperatureChanged(SeekBar seekBar, int temperature, boolean fromUser) { airConditionerModel.changeTemperature(temperature); updateUI(); }
/** * 更新UI時調用 * */ private void updateUI() { temperatureAndWind.set(airConditionerModel.getCurrentCondition()); } }
最後,我們需要在MainActivity中調用JetPack為我們提供的DataBindingUtil類,來幫助我們將佈局文件(activity_main.xml)和對應的ViewModel(MainViewModel)綁定起來。
public class MainActivity extends AppCompatActivity {
private MainViewModel mainViewModel = new MainViewModel();
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
//佈局文件名是activity_main.xml,所以生成的對應的Binding類為ActivityMainBinding //再通過setViewModel()方法,將View(ActivityMainBinding/activity_main.xml文件)和ViewModel綁定起來 //綁定後,便可以在佈局文件中直接調用ViewModel中的方法和欄位 ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); activityMainBinding.setViewModel(mainViewModel);
mainViewModel.onCreate(); }
@Override protected void onPause() { super.onPause(); mainViewModel.onPause(); }
@Override protected void onResume() { super.onResume(); mainViewModel.onResume(); }
@Override protected void onDestroy() { super.onDestroy(); mainViewModel.onDestroy(); } }
可以看到,相對於MVP架構,無需將MainActivity通過構造器傳入Presenter,也就減少了Presenter對MainActivity的引用。相應的,部分原本屬於MainActivity的工作,交給了佈局文件來完成,強化了佈局文件的功能。
XML佈局文件可以直接在編寫佈局文件的時候,指定ViewModel,並將UI控制項與ViewModel中的欄位或者方法綁定。接著,在MainActivity中,通過DataBindingUtil類,完成實際的綁定。這樣當用戶在操作界面的時候,ViewModel依然完成原來的業務邏輯,只不過無需再通知View進行UI更新,因為View中的視圖控制項已經與ViewModel中的數據提前綁定好了。這樣原本屬於MainActivity的部分工作,直接交給了activity_main.xml佈局文件來做。View和Model之間的分層依然是非常明確的。
最後,我們將三種架構的流程圖放在一起對比一下。Model層一直都沒有變化,佈局文件activity_main.xml,在MVVM架構中,由於綁定的需要,加入了<data/>標籤。
對於架構的選擇,我認為沒有絕對的。分層帶來的好處顯而易見,但是也需要編寫更多的代碼,加大了項目的理解難度。對於簡單的頁面或者簡單的邏輯,MVC的架構就夠用了,也是最方便易懂的。架構的出現了是為瞭解決項目層次混亂複雜的問題,如果項目本身並不複雜,完全沒有必要為了架構而架構。