對Android-MVP架構模式的理解與初嘗試

通常,如果你是一名面向對象的開發者,或多或少都瞭解和接觸過大名鼎鼎的“MVC”模式。

到了Android移動端上,因爲其自身的某些特性。於是,從“MVC”模式裏又衍生出了一種新的模式,既“MVP”模式。


關於其二者的特點,從根本來說,十分相似:

  • Controller/Presenter負責接受數據,並命令View或Model做出相應的修改;
  • Model負責封裝應用程序的數據模型及業務邏輯。
  • View負責應用程序的顯示。
而二者之間的最大的區別在於:
  • MVP:View並不直接使用Model,它們之間的通信是通過Presenter(MVC中的Controller)來進行的,所有的交互都發生Presenter內部。
  • MVC:允許View跳過Controller,直接對Model進行訪問


那麼,到了Android開發當中,爲什麼MVP模式會顯得比MVC模式更加合適,我們加以分析。


以傳統的JavaWeb開發而言,其對應於MVC模式來說,通常表現爲:

  • Servlet或者Filter作爲Controller;
  • JSP作爲View;
  • POJO + 封裝BIZ的Action類作爲Model。


我們按照同樣的思想換算到Android當中,則應該表現爲:

  • Activity作爲Controller;
  • 佈局xml文件作爲View;
  • POJO + BIZ作爲Model。


這時,細心的人可能已經注意到了,強行把Activity作爲Controller似乎並不那麼合理。

因爲在Android當中:Activity除了負責接收用戶的輸入之外,還承擔着加載界面佈局,實例化控件,綁定監聽事件等等工作。


你之所以選擇架構模式,根本就是爲了最大程度的對代碼結構進行解耦和模塊分離。

所以,如果出現Activity既像View,又像Controller這種不倫不類的情況,我們自然應該避免其發生。


也正是基於此,所以在衍生出的MVP模式中,選擇這樣做。

  • 將Activity作爲View,只負責界面顯示和用戶交互。
  • Model依然保持本色。
  • 而建立Presenter負責View與Model的交互。


這個週末抽空花了一點時間,嘗試了一下所謂的MVP模式。多多少少有些體會,在此記錄。


首先,既然要使用到MVP模式,自然要了解爲什麼使用它。如果沒有好處,我們用它搞毛呢?

簡單歸納,其好處爲:

  • 易於維護
  • 易於測試
  • 松耦合度
  • 複用性高
  • 健壯穩定
個人嘗試用這種模式搭建項目結構,發現的確邏輯條理和項目結構更加清晰,會使得後期更容易進行維護工作。

不過相對於平常來說,經分離之後,類文件,接口文件反而相對卻變多了。

所以個人感覺如果是規模較小,邏輯較爲簡單的項目,其實反而沒有使用的必要。


話入正題,所謂的M-V-P,對應 來說:

M - Model數據模型(業務邏輯層)

既是主要處理伴隨數據模型的業務邏輯層。


V - View 視圖(表現層)

更基本的來說,也就是界面,負責顯示數據及與用戶進行交互。


P - Presenter 表示器。

個人認爲可以理解爲一箇中轉站,也就是M與P的連接樞紐。

其行爲表現爲:接收“V”上反饋的指令,分發給“M”進行處理,最後將處理的結果再反饋在“V”上。


知道了較爲書面化的概念,結合一點生活中熟悉的事物,希望能幫助我們更好的進行理解。

我們這樣想象,你開了一家小餐館,那麼對應來說:

V - 餐館門市。

既消費者雙眼所見的,門市既是整個“餐館系統”的視圖。


M - 餐館廚房。

用戶在“V”裏,既是在“餐館”內產生了交互行爲“點餐”,“廚房”負責處理該業務邏輯,最終產生要反饋給用戶的結果“菜餚”。


P - 餐館店小二。

通常來說,食客來到餐館,不會直接開始尋找廚房,然後直接告訴廚師自己所點的菜餚。而廚師也不會在完成菜品之後,再親自上菜。

所以,你需要一個“店小二”,既MVP裏的“P”。這裏你就能十分形象的理解到“表示器P”其充當的角色,既一個“樞紐”的角色。


分析一下:

  1. 食客點餐完畢。
  2. 將菜單交至店小二;
  3. 店小二將菜單送到廚房;
  4. 廚房根據菜單做好菜餚,通知店小二;
  5. 店小二將菜餚上至對應餐桌。

對應來說:

  1. 用戶在android設備上(既“V”)進行了操作;
  2. “V”接受到交互,將用戶的行爲傳遞給表示器“P”;
  3. 表示器收到反饋行爲,通知對應的業務邏輯處理器;
  4. “M”收到,進行處理,將處理得到的結果返回給“P”;
  5. “P”得到結果,將結果返回給“V”,即反饋給用戶;

這樣加以對應,是否會對於理解MVP模式有所幫助?


話已至此,對於所謂的MVP,我們基本已經有了一定的瞭解,接着,就根據一個簡單的例子,來加深自己的印象和理解。
以我在另一篇描述MVP模式的博客裏看到的需求爲例,假設我們現在有一個“驗證登錄”的需求,要實現以下效果:



接着,開始着手搭建項目,編寫代碼。

1、餐廳開業之前,我們要先搞定門市。OK,所以第一步,我們完成登錄界面的佈局文件的編寫。

2、門市搞定了,爲了生意,接着我們進行“裝修”。沒問題,那我們接着進行View的編寫工作,完成裝修(加載佈局,實例化控件,綁定監聽事件)。於是這時的LoginActivity應該是這樣的:
public class LoginActivity extends Activity implements ILoginView {
	private EditText userName, passWord;
	private Button login, clearInput;
	private ProgressBar loginProgress;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_login);
		// Init view
		initView();

	}

    private void initView() {
	userName = (EditText) this.findViewById(R.id.user_name);
	passWord = (EditText) this.findViewById(R.id.password);

	login = (Button) this.findViewById(R.id.btn_login);
	clearInput = (Button) this.findViewById(R.id.btn_clear);

	login.setOnClickListener(new View.OnClickListener() {

		@Override
		public void onClick(View v) {
			//need to do..
		}
	});

	clearInput.setOnClickListener(new View.OnClickListener() {

		@Override
		public void onClick(View v) {
			//need to do..
		}
	});
	
	loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);
}

	}
}

3.門市的裝修工作終於完成了,我們送了一口氣。這個時候,爲了將來餐館能夠有條不紊的順利營業。此時,也許我們就應該開始着手考慮,當我們的餐館正式開始營業後,可能與消費產生哪些交互行爲?從而定下一些好的營業行爲規範了!
  
  對應於程序中來說,既是我們該View中需要與用戶發生交互的一切行爲。所以,對應我們“驗證登錄”的需求來說,接口的定義可能就類似於:
public interface ILoginView {

	// 獲取用戶輸入用戶名
	String getUserNameInput();

	// 獲取用戶輸入密碼
	String getPassWordInput();

	// 登錄成功,跳轉界面
	void toAppMainActivity();

	// 登錄失敗,提示用戶
	void showLoginFailedInfo();

	// 清空輸入記錄
	void clearInputValue();
	
	// 提示正在登錄
	void showLoginLoading();
	
	// 隱藏正在登錄提示
	void hideLoginLoading();
}

4、規範我們已經定下了,通知小祕,去把經營規範打印出來,給我掛在餐館裏最顯眼的地方!
所以我們此時要做的也是一樣,既然View的接口已經聲明完畢,此時我們就應該讓Activity去實現接口,去落實老闆定下的規範。
所以此時的Activity類經過完善,變成了下面這樣:
public class LoginActivity extends Activity implements ILoginView {

	private EditText userName, passWord;
	private Button login, clearInput;
	private ProgressBar loginProgress;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_login);
		// Init view
		initView();

	}

	private void initView() {
		userName = (EditText) this.findViewById(R.id.user_name);
		passWord = (EditText) this.findViewById(R.id.password);

		login = (Button) this.findViewById(R.id.btn_login);
		clearInput = (Button) this.findViewById(R.id.btn_clear);

		login.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				// need to do..
			}
		});

		clearInput.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				// need to do..
			}
		});
		
		loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);
	}

	// implements interface method
	@Override
	public String getUserNameInput() {
		return userName.getEditableText().toString();
	}

	@Override
	public String getPassWordInput() {
		return passWord.getEditableText().toString();
	}

	@Override
	public void toAppMainActivity() {
		Intent intent = new Intent(LoginActivity.this, MainActivity.class);
		startActivity(intent);

	}

	@Override
	public void showLoginFailedInfo() {
		Toast.makeText(this, "Sorry,Plase check your input!", Toast.LENGTH_LONG).show();
	}

	@Override
	public void clearInputValue() {
		userName.setText("");
		passWord.setText("");
	}

	@Override
	public void showLoginLoading() {
		loginProgress.setVisibility(View.VISIBLE);
	}

	@Override
	public void hideLoginLoading() {
		loginProgress.setVisibility(View.INVISIBLE);
	}
}

5、這個時候對於店鋪的打理已經基本進行完畢了,這個時候我們應該着手考慮一下餐廳的業務邏輯了,例如最關鍵的:找個好廚師~
沒問題,也就是說此時我們應該開始着手於Model層的打架工作了。
在這裏,對應於驗證登錄的業務需求,我們首先建立我們的數據模型:
public class User {

	private String userName;
	private String passWord;

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassWord() {
		return passWord;
	}

	public void setPassWord(String passWord) {
		this.passWord = passWord;
	}

}
數據模型搞定之後,也就該完成針對於此數據模型進行處理的業務邏輯類了。在我們的例子中,只存在一個業務邏輯,既是驗證登錄。首先,我們依舊是首先定義下規範:
public interface ILoginModel {

	void doLogin(String userName, String passWord, ILoginListener loginListener);
}
接着,根據規範去定義具體的處理辦法:
public class LoginModel implements ILoginModel {

	@Override
	public void doLogin(final String userName, final String passWord, final ILoginListener loginListener) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				// 模擬耗時操作
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				// 驗證登錄
				if (userName.equals("tsr") && passWord.equals("123")) {
					User user = new User();
					user.setUserName(userName);
					user.setPassWord(passWord);
					loginListener.loginSuccess(user);
				} else {
					loginListener.loginFailed();
				}

			}
		}).start();
	}

}
一切工作都在有條不紊的進行當中,唯一值得注意的是,在此我們定義了一個監聽接口,用以監聽登錄結果,並根據結果做出對應操作。
public interface ILoginListener {

	void loginSuccess(User user);

	void loginFailed();
}

6、當我們工作至此處時,廚房部門已經被我們打點完畢了。此時,萬事俱備,只欠東風。我們只差一個關鍵的“店小二”了。
是的,當View層和Model層的搭建工作都已經完成,我們就只差一個Presenter來聯繫它們了。

你可以試着這樣考慮,既然Presenter將作爲View和Model的鏈接樞紐,那麼肯定它是能夠同時訪問二者的。
這對應於程序來說,也就是,在Presenter類裏,將必然存在View和Model的一個實例,從而你才能夠調用他們的方法,完成交互。

而同時,就好比你作爲餐館老闆招聘一名店小二,你肯定會明確的做出要求:既店小二開始上班以後,他的工作內容是什麼。
所以在Presenter類裏,你還應該根據實際,定義相應的行爲。
就如同,餐館老闆在餐廳營業應爲裏規定了有“點餐”這個營業行爲,那麼當有顧客進行了“點餐”這個行爲時,就應該調用“店小二”執行“收、取菜單”的動作。
對應我們本例當中,既是當有用戶點擊了“登錄”按鈕時,View就知道調用Presenter裏的對應方法,再由Presenter去調用model裏對應的方法。

所以,我們最終的Presenter類的定義可能如下:
public class LoginPresenter {

	private ILoginModel loginModel;
	private ILoginView loginView;
	private Handler mHandler;

	public LoginPresenter(ILoginView loginView) {
		this.loginModel = new LoginModel();
		this.loginView = loginView;
		mHandler = new Handler();
	}

	public void doLogin() {
		loginView.showLoginLoading();
		
		loginModel.doLogin(loginView.getUserNameInput(), loginView.getPassWordInput(), new ILoginListener() {

			@Override
			public void loginSuccess(User user) {
				mHandler.post(new Runnable() {

					@Override
					public void run() {
						loginView.hideLoginLoading();
						loginView.toAppMainActivity();

					}
				});

			}

			@Override
			public void loginFailed() {
				mHandler.post(new Runnable() {

					@Override
					public void run() {
						loginView.hideLoginLoading();
						loginView.showLoginFailedInfo();

					}
				});

			}
		});
	}

	public void clearInputValue() {
		loginView.clearInputValue();
	}
}

7、到了這裏你的餐廳完全已經做好開始營業的準備了!唯一的工作,記得爲你的“店小二”辦理入職手續。
對應到我們的程序來說,既是將Presenter的實例添加到View當中,並且在各個按鈕的監聽事件裏,安排Presenter進行對應的行爲。
於是,最終的Activity變成了:
public class LoginActivity extends Activity implements ILoginView {

	private LoginPresenter mPresenter;
	private EditText userName, passWord;
	private Button login, clearInput;
	private ProgressBar loginProgress;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_login);
		// Init presenter
		mPresenter = new LoginPresenter(this);
		// Init view
		initView();

	}

	private void initView() {
		userName = (EditText) this.findViewById(R.id.user_name);
		passWord = (EditText) this.findViewById(R.id.password);

		login = (Button) this.findViewById(R.id.btn_login);
		clearInput = (Button) this.findViewById(R.id.btn_clear);

		login.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				mPresenter.doLogin();
			}
		});

		clearInput.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				mPresenter.clearInputValue();
			}
		});
		
		loginProgress = (ProgressBar) this.findViewById(R.id.login_loading);
	}

	// implements interface method
	@Override
	public String getUserNameInput() {
		return userName.getEditableText().toString();
	}

	@Override
	public String getPassWordInput() {
		return passWord.getEditableText().toString();
	}

	@Override
	public void toAppMainActivity() {
		Intent intent = new Intent(LoginActivity.this, MainActivity.class);
		startActivity(intent);

	}

	@Override
	public void showLoginFailedInfo() {
		Toast.makeText(this, "Sorry,Plase check your input!", Toast.LENGTH_LONG).show();
	}

	@Override
	public void clearInputValue() {
		userName.setText("");
		passWord.setText("");
	}

	@Override
	public void showLoginLoading() {
		loginProgress.setVisibility(View.VISIBLE);
	}

	@Override
	public void hideLoginLoading() {
		loginProgress.setVisibility(View.INVISIBLE);
	}
}

至此,我們已經完全採用M-V-P的架構模式來搭建完成了“登錄驗證”的小項目。
對此我們加以總結,其實可以得出:對於MVP模式的構建,其規律爲:
  • Activity現在只編寫最基本的功能代碼,如:加載佈局,實例化控件,綁定監聽等。
  • 將與用戶發生交互的界面操作,都抽象到View接口中,然後由Activity實現接口,進而具體實現。
  • 需要對用戶的行爲進行處理或反饋的方法(例如上面我們的代碼中登錄與清楚按鈕的點擊事件處理),都抽離出來放到Presenter當中。
  • 需要對針對於數據模型進行操作的方法(例如將用戶輸入的數據進行持久化存儲或需要在服務器進行處理等)都放在業務邏輯類,即Model當中。
  • Presenter從View接收數據,調用Model進行處理;從Model獲取處理的結果,調用View反饋到界面。
來看一下最終的代碼結構變成了什麼樣,是不是條理有變得更加清晰:






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章