Android進階十八 MVC、MVP、MVVM架構總結
一、MVVM概述
MVVM是一種軟體開發架構,是Model-View-View Model的縮寫,在Android中要實現MVVM架構,
需要使用Databinding的框架,Databinding即資料繫結,是Google為了能在Android上實現MVVM而設計的一套框架,通過它,可以進行MVVM中的ViewModel和View的單向和雙向繫結,
這樣View(Activity,Fragment等)只單純的做UI顯示的事情,ViewModel則用於處理邏輯,以下是一些Databinding的一些總結:
二、MVC、MVP、MVVM
首先,我們先大致瞭解Android開發中常見的模式,以便我們更深入瞭解MVVM 模式。
MVC
MVC,Model View Controller,是軟體架構中最常見的一種框架,簡單來說就是通過controller的控制去操作model層的資料,並且返回給view層展示,具體見下圖
View:對應於xml佈局檔案,顯示Model層的資料結果
Model:適合做一些業務邏輯處理,比如資料庫存取操作,網路操作,複雜的演算法,耗時的任務等都在model層處理。
Controllor:對應於Activity業務邏輯,資料處理和UI處理
當用戶出發事件的時候,view層會發送指令到controller層,接著controller去通知model層更新資料,model層更新完資料以後直接顯示在view層上,這就是MVC的工作原理。
下面看一個獲取天氣預報的例子:
Controller控制器
package com.xjp.androidmvcdemo.controller;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.xjp.androidmvcdemo.R;
import com.xjp.androidmvcdemo.entity.Weather;
import com.xjp.androidmvcdemo.entity.WeatherInfo;
import com.xjp.androidmvcdemo.model.OnWeatherListener;
import com.xjp.androidmvcdemo.model.WeatherModel;
import com.xjp.androidmvcdemo.model.WeatherModelImpl;
public class MainActivity extends ActionBarActivity implements OnWeatherListener, View.OnClickListener {
private WeatherModel weatherModel;
private Dialog loadingDialog;
private EditText cityNOInput;
private TextView city;
private TextView cityNO;
private TextView temp;
private TextView wd;
private TextView ws;
private TextView sd;
private TextView wse;
private TextView time;
private TextView njd;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
weatherModel = new WeatherModelImpl();
initView();
}
/**
* 初始化View
*/
private void initView() {
cityNOInput = findView(R.id.et_city_no);
city = findView(R.id.tv_city);
cityNO = findView(R.id.tv_city_no);
temp = findView(R.id.tv_temp);
wd = findView(R.id.tv_WD);
ws = findView(R.id.tv_WS);
sd = findView(R.id.tv_SD);
wse = findView(R.id.tv_WSE);
time = findView(R.id.tv_time);
njd = findView(R.id.tv_njd);
findView(R.id.btn_go).setOnClickListener(this);
loadingDialog = new ProgressDialog(this);
loadingDialog.setTitle("載入天氣中...");
}
/**
* 顯示結果
*
* @param weather
*/
public void displayResult(Weather weather) {
WeatherInfo weatherInfo = weather.getWeatherinfo();
city.setText(weatherInfo.getCity());
cityNO.setText(weatherInfo.getCityid());
temp.setText(weatherInfo.getTemp());
wd.setText(weatherInfo.getWD());
ws.setText(weatherInfo.getWS());
sd.setText(weatherInfo.getSD());
wse.setText(weatherInfo.getWSE());
time.setText(weatherInfo.getTime());
njd.setText(weatherInfo.getNjd());
}
/**
* 隱藏進度對話方塊
*/
public void hideLoadingDialog() {
loadingDialog.dismiss();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_go:
loadingDialog.show();
weatherModel.getWeather(cityNOInput.getText().toString().trim(), this);
break;
}
}
@Override
public void onSuccess(Weather weather) {
hideLoadingDialog();
displayResult(weather);
}
@Override
public void onError() {
hideLoadingDialog();
Toast.makeText(this, "獲取天氣資訊失敗", Toast.LENGTH_SHORT).show();
}
private <T extends View> T findView(int id) {
return (T) findViewById(id);
}
}
從上面程式碼可以看到,Activity持有了WeatherModel模型的物件,當用戶有點選Button互動的時候,Activity作為Controller控制層讀取View檢視層EditTextView的資料,然後向Model模型發起資料請求,也就是呼叫WeatherModel物件的方法 getWeathre()方法。當Model模型處理資料結束後,通過介面OnWeatherListener通知View檢視層資料處理完畢,View檢視層該更新介面UI了。然後View檢視層呼叫displayResult()方法更新UI。至此,整個MVC框架流程就在Activity中體現出來了。
Model模型
來看看WeatherModelImpl程式碼實現
package com.xjp.androidmvcdemo.model;
/**
* Description:請求網路資料介面
* User: xjp
* Date: 2015/6/3
* Time: 15:40
*/
public interface WeatherModel {
void getWeather(String cityNumber, OnWeatherListener listener);
}
................
package com.xjp.androidmvcdemo.model;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.xjp.androidmvcdemo.entity.Weather;
import com.xjp.androidmvcdemo.volley.VolleyRequest;
/**
* Description:從網路獲取天氣資訊介面實現
* User: xjp
* Date: 2015/6/3
* Time: 15:40
*/
public class WeatherModelImpl implements WeatherModel {
@Override
public void getWeather(String cityNumber, final OnWeatherListener listener) {
/*資料層操作*/
VolleyRequest.newInstance().newGsonRequest("http://www.weather.com.cn/data/sk/" + cityNumber + ".html",
Weather.class, new Response.Listener<Weather>() {
@Override
public void onResponse(Weather weather) {
if (weather != null) {
listener.onSuccess(weather);
} else {
listener.onError();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
listener.onError();
}
});
}
}
以上程式碼看出,這裡設計了一個WeatherModel模型介面,然後實現了介面WeatherModelImpl類。controller控制器activity呼叫WeatherModelImpl類中的方法發起網路請求,然後通過實現OnWeatherListener介面來獲得網路請求的結果通知View檢視層更新UI 。至此,Activity就將View檢視顯示和Model模型資料處理隔離開了。activity擔當contronller完成了model和view之間的協調作用。
至於這裡為什麼不直接設計成類裡面的一個getWeather()方法直接請求網路資料?你考慮下這種情況:現在程式碼中的網路請求是使用Volley框架來實現的,如果哪天老闆非要你使用Afinal框架實現網路請求,你怎麼解決問題?難道是修改 getWeather()方法的實現? no no no,這樣修改不僅破壞了以前的程式碼,而且還不利於維護, 考慮到以後程式碼的擴充套件和維護性,我們選擇設計介面的方式來解決著一個問題,我們實現另外一個WeatherModelWithAfinalImpl類,繼承自WeatherModel,重寫裡面的方法,這樣不僅保留了以前的WeatherModelImpl類請求網路方式,還增加了WeatherModelWithAfinalImpl類的請求方式。Activity呼叫程式碼無需要任何修改。
問題
大家想過這樣會有什麼問題嗎?顯然是有的,不然為什麼會有MVP和MVVM的誕生呢,是吧。問題就在於xml作為view層,控制能力實在太弱了,你想去動態的改變一個頁面的背景,或者動態的隱藏/顯示一個按鈕,這些都沒辦法在xml中做,只能把程式碼寫在activity中,造成了activity既是controller層,又是view層的這樣一個窘境。大家回想一下自己寫的程式碼,如果是一個邏輯很複雜的頁面,activity或者fragment是不是動輒上千行呢?這樣不僅寫起來麻煩,維護起來更是噩夢。(當然看過Android原始碼的同學其實會發現上千行的程式碼不算啥,一個RecyclerView.class的程式碼都快上萬行了呢。。)
MVC還有一個重要的缺陷,大家看上面那幅圖,view層和model層是相互可知的,這意味著兩層之間存在耦合,耦合對於一個大型程式來說是非常致命的,因為這表示開發,測試,維護都需要花大量的精力。
正因為MVC有這樣那樣的缺點,所以才演化出了MVP和MVVM這兩種框架。
MVP
View: 對應於Activity和xml,負責View的繪製以及與使用者互動
Model: 依然是實體模型
Presenter: 負責完成View於Model間的互動和業務邏輯
MVP作為MVC的演化,解決了MVC不少的缺點,對於Android來說,MVP的model層相對於MVC是一樣的,而activity和fragment不再是controller層,而是純粹的view層,所有關於使用者事件的轉發全部交由presenter層處理。下面還是讓我們看圖:
首先MVP各部分之間的通訊都是雙向的,但是唯獨View與Model之間是不發生聯絡的,二者之間的通訊都是通過Presenter傳遞的。
MVP的要點:
1、模型與檢視完全分離,我們可以修改檢視而不影響模型
2、可以更高效地使用模型,因為所有的互動都發生在一個地方——Presenter內部
3、我們可以將一個Presenter用於多個檢視,而不需要改變Presenter的邏輯。這個特性非常的有用,因為檢視的變化總是比模型的變化頻繁。
4、如果我們把邏輯放在Presenter中,那麼我們就可以脫離使用者介面來測試這些邏輯(單元測試)
MVP例子
model層
package com.nsu.edu.androidmvpdemo.login;
/**
* Created by Anthony on 2016/2/15.
* Class Note:模擬登陸的操作的介面,實現類為LoginModelImpl.相當於MVP模式中的Model層
*/
public interface LoginModel {
void login(String username, String password, OnLoginFinishedListener listener);
}
package com.nsu.edu.androidmvpdemo.login;
import android.os.Handler;
import android.text.TextUtils;
/**
* Created by Anthony on 2016/2/15.
* Class Note:延時模擬登陸(2s),如果名字或者密碼為空則登陸失敗,否則登陸成功
*/
public class LoginModelImpl implements LoginModel {
@Override
public void login(final String username, final String password, final OnLoginFinishedListener listener) {
new Handler().postDelayed(new Runnable() {
@Override public void run() {
boolean error = false;
if (TextUtils.isEmpty(username)){
listener.onUsernameError();//model層裡面回撥listener
error = true;
}
if (TextUtils.isEmpty(password)){
listener.onPasswordError();
error = true;
}
if (!error){
listener.onSuccess();
}
}
}, 2000);
}
}
View層
負責顯示資料、提供友好介面跟使用者互動就行。MVP下Activity和Fragment以及View的子類體現在了這一 層,Activity一般也就做載入UI檢視、設定監聽再交由Presenter處理的一些工作,所以也就需要持有相應Presenter的引用。本層所需要做的操作就是在每一次有相應互動的時候,呼叫presenter的相關方法就行。(比如說,button點選)
package com.nsu.edu.androidmvpdemo.login;
/**
* Created by Anthony on 2016/2/15.
* Class Note:登陸View的介面,實現類也就是登陸的activity
*/
public interface LoginView {
void showProgress();
void hideProgress();
void setUsernameError();
void setPasswordError();
void navigateToHome();
}
package com.nsu.edu.androidmvpdemo.login;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.nsu.edu.androidmvpdemo.R;
/**
* Created by Anthony on 2016/2/15.
* Class Note:MVP模式中View層對應一個activity,這裡是登陸的activity
*/
public class LoginActivity extends Activity implements LoginView, View.OnClickListener {
private ProgressBar progressBar;
private EditText username;
private EditText password;
private LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
progressBar = (ProgressBar) findViewById(R.id.progress);
username = (EditText) findViewById(R.id.username);
password = (EditText) findViewById(R.id.password);
findViewById(R.id.button).setOnClickListener(this);
presenter = new LoginPresenterImpl(this);
}
@Override
protected void onDestroy() {
presenter.onDestroy();
super.onDestroy();
}
@Override
public void showProgress() {
progressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideProgress() {
progressBar.setVisibility(View.GONE);
}
@Override
public void setUsernameError() {
username.setError(getString(R.string.username_error));
}
@Override
public void setPasswordError() {
password.setError(getString(R.string.password_error));
}
@Override
public void navigateToHome() {
// TODO startActivity(new Intent(this, MainActivity.class));
Toast.makeText(this,"login success",Toast.LENGTH_SHORT).show();
// finish();
}
@Override
public void onClick(View v) {
presenter.validateCredentials(username.getText().toString(), password.getText().toString());
}
}
Presenter層
Presenter扮演著view和model的中間層的角色。獲取model層的資料之後構建view層;也可以收到view層UI上的反饋命令後分發處理邏輯,交給model層做業務操作。它也可以決定View層的各種操作。
package com.nsu.edu.androidmvpdemo.login;
/**
* Created by Anthony on 2016/2/15.
* Class Note:登陸的Presenter 的介面,實現類為LoginPresenterImpl,完成登陸的驗證,以及銷燬當前view
*/
public interface LoginPresenter {
void validateCredentials(String username, String password);
void onDestroy();
}
package com.nsu.edu.androidmvpdemo.login;
/**
* Created by Anthony on 2016/2/15.
* Class Note:
* 1 完成presenter的實現。這裡面主要是Model層和View層的互動和操作。
* 2 presenter裡面還有個OnLoginFinishedListener,
* 其在Presenter層實現,給Model層回撥,更改View層的狀態,
* 確保 Model層不直接操作View層。如果沒有這一介面在LoginPresenterImpl實現的話,
* LoginPresenterImpl只 有View和Model的引用那麼Model怎麼把結果告訴View呢?
*/
public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener {
private LoginView loginView;
private LoginModel loginModel;
public LoginPresenterImpl(LoginView loginView) {
this.loginView = loginView;
this.loginModel = new LoginModelImpl();
}
@Override
public void validateCredentials(String username, String password) {
if (loginView != null) {
loginView.showProgress();
}
loginModel.login(username, password, this);
}
@Override
public void onDestroy() {
loginView = null;
}
@Override
public void onUsernameError() {
if (loginView != null) {
loginView.setUsernameError();
loginView.hideProgress();
}
}
@Override
public void onPasswordError() {
if (loginView != null) {
loginView.setPasswordError();
loginView.hideProgress();
}
}
@Override
public void onSuccess() {
if (loginView != null) {
loginView.navigateToHome();
}
}
}
登陸的回撥介面:
package com.nsu.edu.androidmvpdemo.login;
/**
* Created by Anthony on 2016/2/15.
* Class Note:登陸事件監聽
*/
public interface OnLoginFinishedListener {
void onUsernameError();
void onPasswordError();
void onSuccess();
}
demo的程式碼流程:(請參考下面的類圖)
- Activity做了一些UI初始化的東西並需要例項化對應LoginPresenter的引用和實現 LoginView的介面,監聽介面動作;
- 登陸按鈕按下後即接收到登陸的事件,在onClick裡接收到即通過LoginPresenter的引用把它交給LoginPresenter處理。LoginPresenter接收到了登陸的邏輯就知道要登陸了;
- 然後LoginPresenter顯示進度條並且把邏輯交給我們的Model去處理,也就是這裡面的LoginModel,(LoginModel的實現類LoginModelImpl),同時會把OnLoginFinishedListener也就是LoginPresenter自身傳遞給我們的Model(LoginModel);
- LoginModel處理完邏輯之後,結果通過OnLoginFinishedListener回撥通知LoginPresenter;
- LoginPresenter再把結果返回給view層的Activity,最後activity顯示結果
請參考這張類圖:
注意
1 presenter裡面還有個OnLoginFinishedListener,其在Presenter層實現,給Model層回撥,更改View層的狀態,確保 Model層不直接操作View層。
2 在一個好的架構中,model層可能只是一個領域層和業務邏輯層的入口,如果我們參考網上比較火的Uncle Bob clean architecture model層可能是一個實現業務用例的互動者,在後續的文章中應該會涉及到這方面的問題,目前能力有限。
MVVM
MVVM最早是由微軟提出的
從圖中看出,它和MVP的區別貌似不大,只不過是presenter層換成了viewmodel層,還有一點就是view層和viewmodel層是相互繫結的關係,這意味著當你更新viewmodel層的資料的時候,view層會相應的變動ui。
我們很難去說MVP和MVVM這兩個MVC的變種孰優孰劣,還是要具體情況具體分析。
資料驅動
在MVVM中,以前開發模式中必須先處理業務資料,然後根據的資料變化,去獲取UI的引用然後更新UI,通過也是通過UI來獲取使用者輸入,而在MVVM中,資料和業務邏輯處於一個獨立的View Model中,ViewModel只要關注資料和業務邏輯,不需要和UI或者控制元件打交道。由資料自動去驅動UI去自動更新UI,UI的改變又同時自動反饋到資料,資料成為主導因素,這樣使得在業務邏輯處理只要關心資料,方便而且簡單很多。
低耦合度
MVVM模式中,資料是獨立於UI的,ViewModel只負責處理和提供資料,UI想怎麼處理資料都由UI自己決定,ViewModel 不涉及任何和UI相關的事也不持有UI控制元件的引用,即使控制元件改變(TextView 換成 EditText)ViewModel 幾乎不需要更改任何程式碼,專注自己的資料處理就可以了,如果是MVP遇到UI更改,就可能需要改變獲取UI的方式,改變更新UI的介面,改變從UI上獲取輸入的程式碼,可能還需要更改訪問UI物件的屬性程式碼等等。
更新 UI
在MVVM中,我們可以在工作執行緒中直接修改View Model的資料(只要資料是執行緒安全的),剩下的資料繫結框架幫你搞定,很多事情都不需要你去關心。
團隊協作
MVVM的分工是非常明顯的,由於View和View Model之間是鬆散耦合的。一個是處理業務和資料,一個是專門的UI處理。完全有兩個人分工來做,一個做UI(xml 和 Activity)一個寫ViewModel,效率更高。
可複用性
一個View Model複用到多個View中,同樣的一份資料,用不同的UI去做展示,對於版本迭代頻繁的UI改動,只要更換View層就行,對於如果想在UI上的做AbTest 更是方便的多。
單元測試
View Model裡面是資料和業務邏輯,View中關注的是UI,這樣的做測試是很方便的,完全沒有彼此的依賴,不管是UI的單元測試還是業務邏輯的單元測試,都是低耦合的。
通過上面對MVVM的簡述和其他兩種模式的對比,我們發現MVVM對比MVC和MVP來說還是存在比較大的優勢,雖然目前Android開發中可能真正在使用MVVM的很少,但是是值得我們去做一些探討和調研。
如何構建MVVM應用程式
1. 如何分工
構建MVVM框架首先要具體瞭解各個模組的分工,接下來我們來講解View,ViewModel,Model 的它們各自的職責所在。
View
View層做的就是和UI相關的工作,我們只在XML和Activity或Fragment寫View層的程式碼,View層不做和業務相關的事,也就是我們的Activity 不寫和業務邏輯相關程式碼,也不寫需要根據業務邏輯來更新UI的程式碼,因為更新UI通過Binding實現,更新UI在ViewModel裡面做(更新繫結的資料來源即可),Activity 要做的事就是初始化一些控制元件(如控制元件的顏色,新增 RecyclerView 的分割線),Activity可以更新UI,但是更新的UI必須和業務邏輯和資料是沒有關係的,只是單純的根據點選或者滑動等事件更新UI(如 根據滑動顏色漸變、根據點選隱藏等單純UI邏輯),Activity(View層)是可以處理UI事件,但是處理的只是處理UI自己的事情,View層只處理View層的事。簡單的說:View層不做任何業務邏輯、不涉及操作資料、不處理資料、UI和資料嚴格的分開。
ViewModel
ViewModel層做的事情剛好和View層相反,ViewModel 只做和業務邏輯和業務資料相關的事,不做任何和UI、控制元件相關的事,ViewModel 層不會持有任何控制元件的引用,更不會在ViewModel中通過UI控制元件的引用去做更新UI的事情。ViewModel就是專注於業務的邏輯處理,操作的也都是對資料進行操作,這些個資料來源繫結在相應的控制元件上會自動去更改UI,開發者不需要關心更新UI的事情。DataBinding 框架已經支援雙向繫結,這使得我們在可以通過雙向繫結獲取View層反饋給ViewModel層的資料,並進行操作。關於對UI控制元件事件的處理,我們也希望能把這些事件處理繫結到控制元件上,並把這些事件統一化,方便ViewModel對事件的處理和程式碼的美觀。為此我們通過BindingAdapter 對一些常用的事件做了封裝,把一個個事件封裝成一個個Command,對於每個事件我們用一個ReplyCommand去處理就行了,ReplyCommand會把可能你需要的資料帶給你,這使得我們處理事件的時候也只關心處理資料就行了,具體見MVVM Light Toolkit 使用指南的 Command 部分。再強調一遍ViewModel 不做和UI相關的事。
Model
Model 的職責很簡單,基本就是實體模型(Bean)同時包括Retrofit 的Service ,ViewModel 可以根據Model 獲取一個Bean的Observable( RxJava ),然後做一些資料轉換操作和對映到ViewModel 中的一些欄位,最後把這些欄位繫結到View層上。
2. 如何協作
關於協作,我們先來看下面的一張圖:
上圖反應了MVVM框架中各個模組的聯絡和資料流的走向,由上圖可知View和Model 直接是解耦的,是沒有直接聯絡的,也就是我之前說到的View 不做任何和業務邏輯和資料處理相關的事。我們從每個模組一一拆分來看。那麼我們重點就是下面的三個協作。
ViewModel與Model的協作
ViewModel與ViewModel的協作
ViewModel與View的協作
ViewModel 和View 是通過繫結的方式連線在一起的,繫結的一種是資料繫結,一種是命令繫結。資料的繫結 DataBinding 已經提供好了,簡單的定義一些ObservableField就能把資料和控制元件繫結在一起了(如TextView的text屬性),但是DataBinding框架提供的不夠全面,比如說如何讓一個URL繫結到一個ImageView讓這個ImageView能自動去載入url指定的圖片,如何把資料來源和佈局模板繫結到一個ListView,讓ListView可以不需要去寫Adapter和ViewHolder 相關的東西,而只是通過簡單的繫結的方式把ViewModel的資料來源繫結到Xml的控制元件裡面就能快速的展示列表呢?這些就需要我們做一些工作和簡單的封裝。MVVM Light Toolkit 已經幫我們做了一部分的工作,詳情可以檢視MVVM Light Toolkit 使用指南。關於事件繫結也是一樣,MVVM Light Toolkit 做了簡單的封裝,對於每個事件我們用一個ReplyCommand< T >去處理就行了,ReplyCommand< T >會把可能你需要的資料帶給你,這使得我們處理事件的時候也只關心處理資料就行了。
由 圖 1 中ViewModel的模組中我們可以看出ViewModel類下面一般包含下面5個部分:
- Context (上下文)
- Model (資料模型Bean)
- Data Field (資料繫結)
- Command (命令繫結)
- Child ViewModel (子ViewModel)
我們先來看下示例程式碼,然後在一一講解5個部分是幹嘛用的:
//context
private Activity context;
//model(資料模型Bean)
private NewsService.News news;
private TopNewsService.News topNews;
//資料繫結(data field)
public final ObservableField<String> imageUrl = new ObservableField<>();
public final ObservableField<String> html = new ObservableField<>();
public final ObservableField<String> title = new ObservableField<>();
// 一個變數包含了所有關於View Style 相關的欄位
public final ViewStyle viewStyle = new ViewStyle();
//命令繫結(command)
public final ReplyCommand onRefreshCommand = new ReplyCommand<>(() -> {
})
public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((p) -> {
});
//Child ViewModel
public final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();
/** * ViewStyle 關於控制元件的一些屬性和業務資料無關的Style 可以做一個包裹,這樣程式碼比較美觀,
ViewModel 頁面也不會有太多的欄位。 **/
public static class ViewStyle {
public final ObservableBoolean isRefreshing = new ObservableBoolean(true);
public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);
}
Context
Context 是幹嘛用的呢,為什麼每個ViewModel都最好需要持了一個Context的引用呢?ViewModel 不做和UI相關的事,不操作控制元件,也不更新UI,那為什麼要有Context呢?原因主要有以下兩點,當然也有其他用處,呼叫工具類、幫助類可能需要context引數等:
通過圖1中,我們發現ViewModel 通過傳參給Model 然後得到一個Observable,其實這就是網路請求部分,做網路請求我們必須把Retrofit Service返回的Observable繫結到Context的生命週期上,防止在請求回來時Activity已經銷燬等異常,其實這個Context的目的就是把網路請求繫結到當前頁面的生命週期中。
在圖1中,我們可以看到兩個ViewModel 之間的聯絡是通過Messenger來做,這個Messenger 是需要用到Context,這個我們後續會講解。
Model
Model 是什麼呢,其實就是資料原型,也就是我們用Json轉過來的Java Bean,我們可能都知道,ViewModel要把資料對映到View中可能需要大量對Model的資料拷貝,拿Model 的欄位去生成對應的ObservableField(我們不會直接拿Model的資料去做展示),這裡其實是有必要在一個ViewModel 保留原始的Model引用,這對於我們是非常有用的,因為可能使用者的某些操作和輸入需要我們去改變資料來源,可能我們需要把一個Bean 從列表頁點選後傳給詳情頁,可能我們需要把這個model 當做表單提交到伺服器。這些都需要我們的ViewModel持有相應的model。
Data Field (資料繫結)
Data Field 就是需要繫結到控制元件上的ObservableField欄位, 無可厚非這是ViewModel的必須品。這個沒有什麼好說,但是這邊有一個建議:
這些欄位是可以稍微做一下分類和包裹的,比如說可能一些欄位繫結到控制元件的一些Style屬性上(如果說:長度,顏色,大小)這些根據業務邏輯的變化而動態去更改的,對於著一類針對View Style的的欄位可以宣告一個ViewStyle類包裹起來,這樣整個程式碼邏輯會更清晰一些,不然ViewModel裡面可能欄位氾濫,不易管理和閱讀性較差。而對於其他一些欄位,比如說title,imageUrl,name這些屬於資料來源型別的欄位,這些欄位也叫資料欄位,是和業務邏輯息息相關的,這些欄位可以放在一塊。
Command (命令繫結)
Command (命令繫結)說白了就是對事件的處理(下拉重新整理,載入更多,點選,滑動等事件處理),我們之前處理事件是拿到UI控制元件的引用,然後設定Listener,這些Listener 其實就是Command,但是考慮到在一個ViewModel 寫各種Listener 並不美觀,可能實現一個Listener就需要實現多個方法,但是我們可能只想要其中一個有用的方法實現就好了。同時實現Listener 會拿到UI的引用,可能會去做一些和UI相關的事情,這和我們之前說的ViewModel 不持有控制元件的引用,ViewModel不更改UI 有相悖。更重要一點是實現一個Listener 可能需要寫一些UI邏輯才能最終獲取我們想要的,簡單一點的比如說,你想要監聽ListView滑到最底部然後觸發載入更多的事件,這時候你就要在ViewModel裡面寫一個OnScrollListener,然後在裡面的onScroll方法中做計算,計算什麼時候ListView滑動底部了,其實ViewModel的工作並不想去處理這些事件,它專注做的應該是業務邏輯和資料處理,如果有一個東西它不需要你自己去計算是否滑到底部,而是在滑動底部自動觸發一個Command,同時把當前列表的總共的item數量返回給你,方便你通過 page=itemCount/LIMIT+1去計算出應該請求伺服器哪一頁的資料那該多好啊。MVVM Light Toolkit 幫你實現了這一點:
public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> {
int page=itemCount/LIMIT+1;
loadData(page.LIMIT)
});
接著在XML 佈局檔案中通過bind:onLoadMoreCommand繫結上去就行了
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>
具體想了解更多請檢視 MVVM Light Toolkit 使用指南,裡面有比較詳細的講解Command的使用。當然Command並不是必須的,你完全可以依照你的習慣和喜好在ViewModel 寫Listener,不過使用Command 可以使你的ViewModel 更簡潔易讀,你也可以自己定義更多的Command,自己定義其他功能Command,那麼ViewModel的事件處理都是託管ReplyCommand來處理,這樣的程式碼看起來會特別美觀和清晰。
Child ViewModel (子ViewModel)
子ViewModel 的概念就是在ViewModel 裡面巢狀其他的ViewModel,這種場景還是很常見的。比如說你一個Activity裡面有兩個Fragment,ViewModel 是以業務劃分的,兩個Fragment做的業務不一樣,自然是由兩個ViewModel來處理,Activity 本身可能就有個ViewModel 來做它自己的業務,這時候Activity的這個ViewModel裡面可能包含了兩個Fragment分別的ViewModel。這就是巢狀的子ViewModel。還有另外一種就是對於AdapterView 如ListView RecyclerView,ViewPager等。
//Child ViewModelpublic final
ObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();
它們的每個Item 其實就對應於一個ViewModel,然後在當前的ViewModel 通過ObservableList持有引用(如上述程式碼),這也是很常見的巢狀的子ViewModel。我們其實還建議,如果一個頁面業務非常複雜,不要把所有邏輯都寫在一個ViewModel,可以把頁面做業務劃分,把不同的業務放到不同的ViewModel,然後整合到一個總的ViewModel,這樣做起來可以使我們的程式碼業務清晰,簡短意賅,也方便後人的維護。
總得來說ViewModel 和View 之前僅僅只有繫結的關係,View層需要的屬性和事件處理都是在xml裡面繫結好了,ViewModel層不會去操作UI,只會操作資料,ViewModel只是根據業務要求處理資料,這些資料自動對映到View層控制元件的屬性上。關於ViewModel類中包含哪些模組和欄位,這個需要開發者自己去衡量,這邊建議ViewModel 不要引入太多的成員變數,成員變數最好只有上面的提到的5種(context、model、…),能不進入其他型別的變數就儘量不要引進來,太多的成員變數對於整個程式碼結構破壞很大,後面維護的人要時刻關心成員變數什麼時候被初始化,什麼時候被清掉,什麼時候被賦值或者改變,一個細節不小心可能就出現潛在的Bug。太多不清晰定義的成員變數又沒有註釋的程式碼是很難維護的。
我們會把UI控制元件的屬性和事件都通過xml裡面(如bind:[email protected]{…})繫結,但是如果一個業務邏輯要彈一個Dialog,但是你又不想在ViewModel裡面做彈窗的事(ViewModel 不做UI相關的事)或者說改變ActionBar上面的圖示的顏色,改變ActionBar按鈕是否可點選,這些都不是寫在xml裡面(都是用java 初始化話),如何對這些控制元件的屬性做繫結呢?我們先來看下程式碼:
public class MainViewModel implements ViewModel {
....
//true的時候彈出Dialog,false的時候關掉dialog
public final ObservableBoolean isShowDialog = new ObservableBoolean();
....
.....
}
// 在View層做一個對isShowDialog改變的監聽
public class MainActivity extends RxBasePmsActivity {
private MainViewModel mainViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
.....
mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {
if (mainViewModel.isShowDialog.get()) {
dialog.show();
} else {
dialog.dismiss();
}
}
});
}
...
}
簡單的說你可以對任意的ObservableField做監聽,然後根據資料的變化做相應UI的改變,業務層ViewModel 只要根據業務處理資料就行,以資料來驅動UI。
ViewModel與Model的協作
從圖1 中,Model 是通過Retrofit 去獲取網路資料的,返回的資料是一個Observable< Bean >( RxJava ),Model 層其實做的就是這些。那麼ViewModel 做的就是通過傳引數到Model層獲取到網路資料(資料庫同理)然後把Model的部分資料對映到ViewModel的一些欄位(ObservableField),並在ViewModel 保留這個Model的引用,我們來看下這一塊的大致程式碼(程式碼涉及到簡單RxJava,如看不懂可以查閱入門一下):
//Model
private NewsDetail newsDetail;
private void loadData(long id) {
// Observable<Bean> 用來獲取網路資料
Observable<Notification<NewsDetailService.NewsDetail>> newsDetailOb =
RetrofitProvider.getInstance()
.create(NewsDetailService.class)
.getNewsDetail(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
// 將網路請求繫結到Activity 的生命週期
.compose(((ActivityLifecycleProvider) context).bindToLifecycle())
//變成 Notification<Bean> 使我們更方便處理資料和錯誤
.materialize().share();
// 處理返回的資料
newsDetailOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
// 給成員變數newsDetail 賦值,之前提到的5種變數型別中的一種(model型別)
.doOnNext(m -> newsDetail = m)
.subscribe(m -> initViewModelField(m));
// 網路請求錯誤處理
NewsListHelper.dealWithResponseError(
newsDetailOb.filter(Notification::isOnError)
.map(n -> n.getThrowable()));
}
//Model -->ViewModel
private void initViewModelField(NewsDetail newsDetail) {
viewStyle.isRefreshing.set(false);
imageUrl.set(newsDetail.getImage());
Observable.just(newsDetail.getBody())
.map(s -> s + "<style type=\"text/css\">" + newsDetail.getCssStr())
.map(s -> s + "</style>")
.subscribe(s -> html.set(s));
title.set(newsDetail.getTitle());
}
以上程式碼基本把註釋補全了,基本思路比較清晰,,Rxjava涉及的操作符都是比較基本的,如有不懂,可以稍微去入門,之後的原始碼裡面ViewModel資料邏輯處理都是用Rxjava做,所以需要提前學習一下方便你看懂原始碼。
注:我們推薦使用MVVM 和 RxJava一塊使用,雖然兩者皆有觀察者模式的概念,但是我們RxJava不使用在針對View的監聽,更多是業務資料流的轉換和處理。DataBinding框架其實是專用於View-ViewModel的動態繫結的,它使得我們的ViewModel 只需要關注資料,而RxJava 提供的強大資料流轉換函式剛好可以用來處理ViewModel中的種種資料,得到很好的用武之地,同時加上Lambda表示式結合的鏈式程式設計,使ViewModel 的程式碼非常簡潔同時易讀易懂。
ViewModel與ViewModel的協作
在圖 1 中 我們看到兩個ViewModel 之間用一條虛線連線著,中間寫著Messenger,Messenger 可以理解是一個全域性訊息通道,引入messenger最主要的目的就實現ViewModel和ViewModel的通訊,也可以用做View和ViewModel的通訊,但是並不推薦這樣做。ViewModel主要是用來處理業務和資料的,每個ViewModel都有相應的業務職責,但是在業務複雜的情況下,可能存在交叉業務,這時候就需要ViewModel和ViewModel交換資料和通訊,這時候一個全域性的訊息通道就很重要的。關於Messenger 的詳細使用方法可以參照 MVVM Light Toolkit 使用指南的 Messenger 部分,這邊給出一個簡單的例子僅供參考:
場景是這樣的,你的MainActivity對應一個MainViewModel,MainActivity 裡面除了自己的內容還包含一個Fragment,這個Fragment 的業務處理對應於一個FragmentViewModel,FragmentViewModel請求伺服器並獲取資料,剛好這個資料MainViewModel也需要用到,我們不可能在MainViewModel重新請求資料,這樣不太合理,這時候就需要把資料傳給MainViewModel,那麼應該怎麼傳,彼此沒有引用或者回調。那麼只能通過全域性的訊息通道Messenger。
FragmentViewModel 獲取訊息後通知MainViewModel 並把資料傳給它:
combineRequestOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
.map(p -> p.first)
.filter(m -> !m.getTop_stories().isEmpty())
.doOnNext(m ->Observable.just(NewsListHelper.isTomorrow(date)).filter(b -> b).subscribe(b -> itemViewModel.clear()))
// 上面的程式碼可以不看,就是獲取網路資料 ,通過send把資料傳過去
.subscribe(m -> Messenger.getDefault().send(m, TOKEN_TOP_NEWS_FINISH));
MainViewModel 接收訊息並處理:
Messenger.getDefault().register(activity, NewsViewModel.TOKEN_TOP_NEWS_FINISH, TopNewsService.News.class, (news) -> {
// to something....
}
在MainActivity onDestroy 取消註冊就行了(不然導致記憶體洩露):
@Override
protected void onDestroy() {
super.onDestroy();
Messenger.getDefault().unregister(this);
}
當然上面的例子也只是簡單的說明下,Messenger可以用在很多場景,通知,廣播都可以,不一定要傳資料,在一定條件下也可以用在View層和ViewModel 上的通訊和廣播。運用範圍特別廣,需要開發者結合實際的業務中去做更深層次的挖掘。
相關推薦
Android進階十八 MVC、MVP、MVVM架構總結
一、MVVM概述 MVVM是一種軟體開發架構,是Model-View-View Model的縮寫,在Android中要實現MVVM架構, 需要使用Databinding的框架,Databinding即資料繫結,是Google為了能在Android上實現MVV
Android進階(十六)子執行緒呼叫Toast報Can't create handler inside thread that has not called Looper.prepare() 錯誤
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
Python進階(十八)-Python3爬蟲小試牛刀之爬取CSDN部落格個人資訊
分享一下我的偶像大神的人工智慧教程!http://blog.csdn.net/jiangjunshow 也歡迎轉載我的文章,轉載請註明出處 https://blog.csdn.net/mm2zzyzzp Python進階(十八)-Python3爬蟲實踐
Android進階(十六)子執行緒呼叫Toast報Can't create handler inside thread that has not called Looper.prepare() 錯誤
原子執行緒呼叫Toast報Can't create handler inside thread that has not called Looper.prepare() 錯誤 今天用子執行緒調Toast報了一個Can't create handler inside thre
MVC,MVP,MVVM架構模式
相關連結: 這篇博簡單分析了一下MVC,MVP,MVVM三種架構。 1. MVC(Model-View-Controller) 功能劃分:View即檢視,表示使用者介面;Model即模型,主要儲存資料;Controller即控制器,負責業務邏輯。 資料
Android進階系列:八、自定義View之音頻抖動動效
final rim 而是 開始 next draw img 點擊 syn 自定義動畫效果——音頻抖動效果 1.繪制一個矩形: 想要繪制一個矩形,繼承View,並重寫onDraw方法即可。復雜一點還可以重寫onMeasure方法和onLayout方法進行大小測量和位置測量。但
Android進階:十一、視頻播放器初體驗
工程 uil focus view @override 成了 tex headers 整理 上一篇文章我們主要講了視頻播放器開發之前需要準備的一直個知識,TextureView,用於對圖像流的處理。這篇文章開始構建一個基礎的視頻播放器。 一、準備工作 在之前的文章已經說過
Android進階:十二、最簡單的方式實現自定義陰影效果
clas new round war port scale dimens tro hdr 網話說UI設計有三寶 :透明,陰影,加圓角。很多UI在做設計的時候都喜歡做卡片形式,然後添加陰影。卡片UI確實挺好看,但是對Android開發者來說,顯示陰影卻並不那麽手到擒來,因為A
Android 程序架構: MVC、MVP、MVVM、Unidirectional、Clean...
不同 概念 可能 十年 tin gettext 聲明 數據 content 摘選自:GUI 應用程序架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Cleanhttps://zhuanlan.zhihu.com/p/26799645 MV
Android中MVC、MVP、MVVM具體解釋
line 業務邏輯 指令 問題 今天 操作 才幹 入口 pre 前言 今天有時間就剛好有想寫關於這幾個名詞。對於我來說。事實上這麽多名詞、思想歸根究竟就是要依據項
Android進階 二十二 設定TextView文字水平垂直居中
設定TextView文字水平垂直居中 有2種方法可以設定TextView文字居中: 一:在xml檔案設定:android:gravity="center" 二:在程式
Android 架構設計:MVC、MVP、MVVM和元件化
MVC、MVP和MVVM是常見的三種架構設計模式,當前MVP和MVVM的使用相對比較廣泛,當然MVC也並沒有過時之說。而所謂的元件化就是指將應用根據業務需求劃分成各個模組來進行開發,每個模組又可以編譯成獨立的APP進行開發。理論上講,元件化和前面三種架構設計不是
android原始碼設計模式——框構模式MVC、MVP、MVVM
一、框架模式、設計模式、架構模式的概念理解 通常來講框架面向於一系列相同行為程式碼的重用,而設計則面向的是一系列相同結構程式碼的重用,通常所說的架構則介於框架與設計之間 二、MVC、MVP、MVVM三種設計模式 2.1、MVC模式,常見的應用模式,
我的Android進階之旅------>Android自定義View來實現解析lrc歌詞並同步滾動、上下拖動、縮放歌詞的功能
前言 最近有個專案有關於播放音樂時候,關於歌詞有以下幾個功能: 1、實現歌詞同步滾動的功能,即歌曲播放到哪句歌詞,就高亮地顯示出正在播放的這個歌詞; 2、實現上下拖動歌詞時候,可以拖動播放器的進度。即可以不停地上下拖動歌詞,
細說Android框架設計三劍客MVC、MVP和MVVM
最近幾年的移動端開發越來越火,功能越來越強大,處理業務越來越複雜,因此對系統擴充套件性的要求越來越高。而為了更好地進行移動端架構設計,我們最常用的就是MVC和MVP,今天本篇部落格就和大家一起聊一聊這兩種框架設計。 MVC框架 MVC的定義
我的Android進階之旅------>Android 關於arm64-v8a、armeabi-v7a、armeabi、x86下的so檔案相容問題
Android 裝置的CPU型別(通常稱為”ABIs”) armeabiv-v7a: 第7代及以上的 ARM 處理器。2011年15月以後的生產的大部分Android裝置都使用它. arm64-v8a: 第8代、64位ARM處理器,很少裝置,三星 Ga
Android進階——MVP從入門到進階
1.定義 MVP的全稱為Model-View-Presenter,即模型-檢視-協調器(主持者) Model:處理資料和業務邏輯等,如:資料庫的操作,資料的請求,資料運算,JavaBean; View:顯示介面,展示結果等,一切與介面相關的,如:XML檔案,Activity,Fragment,D
【android進階篇】Firefly-RK系列(eg:RK3288 RK3368)App實現重啟、靜默安裝應用
本文的方法只是實現手段的一種,不可能完全適用所有裝置哦,試試才知道。 實現重啟 考慮到裝置需要遠端或自動重啟的場景(比如通過遠端推送的方式下發重啟指令、裝置定時重啟緩解資源緊張等),下面提供一種思路: public static void
Android進階 二十七 Android原生擾人煩的佈局
Android原生擾人煩的佈局 在開發Android應用時,UI佈局是一件令人煩惱的事情。下面主要講解一下Android中的介面佈局。 一、線性佈局(LinearLayout) 線性佈局分為:  
Android框架MVC、MVP和MVVM探究(圖解+案例+附原始碼)
1、介紹 MVC、MVP、MVVM這些模式是為了解決開發過程中的實際問題而提出來的,目前作為主流的幾種架構模式而被廣泛使用。本文程式碼 2、瞭解並區分MVC,MVP,MVVM 2.1 MVC MVC,(Model View Controller)