從MVC到MVP,再到MVVM,無論哪種架構,目地都是為了將代碼分到不同的類文件中,讓應用程序的代碼架構變得更清晰,更容易維護。

本文將對MVC,MVP,以及MVVM分別進行詳細的介紹和對比,並放上倉庫地址(同一個項目,分別用3種架構來實現),便於大家理解和掌握。

項目倉庫地址:

MVC MVP MVVM

演示:

MVC

大部分Android工程師開始寫代碼的時候,都會將業務邏輯和視圖交互都放在Activity/Fragment中。也就是說Activity/Fragment實際上承擔了Controller和View的功能。 View層做的工作非常少,通常只是作為佈局文件展示一下界面。下面我們通過代碼示例來講解MVC架構在Android的運用。

  • View層

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>

  • Model層

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;
}
}
}

  • Controller層

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直接通信,最終導致項目邏輯混亂。層和層之間最好通過消息或者回調的方式來進行通信

MVP

前面我們提到了,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)都沒有變化

  • Presenter層

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());
}
}

  • View層

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);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar)
{

}

@Override
public void onStopTrackingTouch(SeekBar seekBar)
{

}
});

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的引用

private MainPresenter mainPresenter = new MainPresenter(this);

當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變成了相互持有的關係

MVVM

為了讓Presenter和MainActivity之間能夠進一步解耦,並且讓View層能承擔更多一些的工作,引入了MVVM架構。由於我們需要應用DataBinding特性,所以要在app的build.gradle中啟用數據綁定。

//啟用dataBinding
dataBinding{
enabled=true
}

注意:Model(AirConditioner)依然沒有變化

  1. 修改佈局文件

但是佈局文件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();
}

@Override
public void onCreate()
{
this.airConditionerModel = new AirConditioner();
updateUI();
}

@Override
public void onPause()
{

}

@Override
public void onResume()
{

}

@Override
public void onDestroy()
{

}

/**
* 溫度發生變化時候調用
* */
public void onTemperatureChanged(SeekBar seekBar, int temperature, boolean fromUser)
{
airConditionerModel.changeTemperature(temperature);
updateUI();
}

/**
* 風力發生變化時候調用
* */
public void onWindLevelChanged()
{
airConditionerModel.changeWindLevel();
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的工作,交給了佈局文件來完成,強化了佈局文件的功能。

MVVM

XML佈局文件可以直接在編寫佈局文件的時候,指定ViewModel,並將UI控制項與ViewModel中的欄位或者方法綁定。接著,在MainActivity中,通過DataBindingUtil類,完成實際的綁定。這樣當用戶在操作界面的時候,ViewModel依然完成原來的業務邏輯,只不過無需再通知View進行UI更新,因為View中的視圖控制項已經與ViewModel中的數據提前綁定好了。這樣原本屬於MainActivity的部分工作,直接交給了activity_main.xml佈局文件來做。View和Model之間的分層依然是非常明確的。

最後,我們將三種架構的流程圖放在一起對比一下。Model層一直都沒有變化,佈局文件activity_main.xml,在MVVM架構中,由於綁定的需要,加入了<data/>標籤。

對於架構的選擇,我認為沒有絕對的。分層帶來的好處顯而易見,但是也需要編寫更多的代碼,加大了項目的理解難度。對於簡單的頁面或者簡單的邏輯,MVC的架構就夠用了,也是最方便易懂的。架構的出現了是為瞭解決項目層次混亂複雜的問題,如果項目本身並不複雜,完全沒有必要為了架構而架構。


推薦閱讀:
相關文章