MVP模式的核心思想:
MVP把Activity中的UI邏輯抽象成View接口,把業務邏輯抽象成功接口,Model類還是原來的Model。
MVC
- 其中View層其實就是程序的UI界面,用于向用戶展示數據以及接收用戶的輸入
- 而Model層就是JavaBean實體類,用于保存實例數據
- Controller控制器用于更新UI界面和數據實例
View層接受用戶的輸入,然后通過Controller修改對應的Model實例;同時,當Model實例的數據發生變化的時候,需要修改UI界面,可以通過Controller更新界面。View層也可以直接更新Model實例的數據,而不用每次都通過Controller,這樣對于一些簡單的數據更新工作會變得方便許多。
MVP
MVP與MVC最不同的一點是M與V是不直接關聯的也是就Model與View不存在直接關系,這兩者之間間隔著的是Presenter層
Model
Model 是用戶界面需要顯示數據的抽象,也可以理解為從業務數據(結果)那里到用戶界面的抽象(Business rule, data access, model classes)
View
視圖這一層體現的很輕薄,負責顯示數據、提供友好界面跟用戶交互就行。MVP下Activity和Fragment體現在了這一層,Activity一般也就做加載UI視圖、設置監聽再交由Presenter處理的一些工作,所以也就需要持有相應Presenter的引用。例如,Activity上滾動列表時隱藏或者顯示Acionbar(Toolbar),這樣的UI邏輯時也應該在這一層。另外在View上輸入的數據做一些判斷時,例如,EditText的輸入數據,假如是簡單的非空判斷則可以作為View層的邏輯,而當需要對EditText的數據進行更復雜的比較時,如從數據庫獲取本地數據進行判斷時明顯需要經過Model層才能返回了,所以這些細節需要自己掂量。
Presenter
Presenter這一層處理著程序各種邏輯的分發,收到View層UI上的反饋命令、定時命令、系統命令等指令后分發處理邏輯交由業務層做具體的業務操作,然后將得到的 Model 給 View 顯示。
這就是MVP模式,現在這樣的話,Activity的工作的簡單了,只用來響應生命周期,其他工作都丟到Presenter中去完成。從上圖可以看出,Presenter是Model和View之間的橋梁,為了讓結構變得更加簡單,View并不能直接對Model進行操作,這也是MVP與MVC最大的不同之處。
優點
- 分離了視圖邏輯和業務邏輯,降低了耦合
- Activity只處理生命周期的任務,代碼變得更加簡潔
- 視圖邏輯和業務邏輯分別抽象到了View和Presenter的接口中去,提高代碼的可閱讀性
- Presenter被抽象成接口,可以有多種具體的實現,所以方便進行單元測試
- 把業務邏輯抽到Presenter中去,避免后臺線程引用著Activity導致Activity的資源無法被系統回收從而引起內存泄露和OOM
代碼變得更加簡潔
使用MVP之后,Activity就能瘦身許多了,基本上只有FindView、SetListener以及Init的代碼。其他的就是對Presenter的調用,還有對View接口的實現。這種情形下閱讀代碼就容易多了,而且你只要看Presenter的接口,就能明白這個模塊都有哪些業務,很快就能定位到具體代碼。Activity變得容易看懂,容易維護,以后要調整業務、刪減功能也就變得簡單許多。
方便進行單元測試
MVP中,由于業務邏輯都在Presenter里,我們完全可以寫一個PresenterTest的實現類繼承Presenter的接口,現在只要在Activity里把Presenter的創建換成PresenterTest,就能進行單元測試了,測試完再換回來即可。萬一發現還得進行測試,那就再換成PresenterTest吧。
避免內存泄露
Android APP 發生OOM的最大原因就是出現內存泄露造成APP的內存不夠用,而造成內存泄露的兩大原因之一就是Activity泄露(Activity Leak)(另一個原因是Bitmap泄露(Bitmap Leak))
Java一個強大的功能就是其虛擬機的內存回收機制,這個功能使得Java用戶在設計代碼的時候,不用像C++用戶那樣考慮對象的回收問題。然而,Java用戶總是喜歡隨便寫一大堆對象,然后幻想著虛擬機能幫他們處理好內存的回收工作。可是虛擬機在回收內存的時候,只會回收那些沒有被引用的對象,被引用著的對象因為還可能會被調用,所以不能回收。
Activity是有生命周期的,用戶隨時可能切換Activity,當APP的內存不夠用的時候,系統會回收處于后臺的Activity的資源以避免OOM。
采用傳統的MV模式,一大堆異步任務和對UI的操作都放在Activity里面,比如你可能從網絡下載一張圖片,在下載成功的回調里把圖片加載到 Activity 的 ImageView 里面,所以異步任務保留著對Activity的引用。這樣一來,即使Activity已經被切換到后臺(onDestroy已經執行),這些異步任務仍然保留著對Activity實例的引用,所以系統就無法回收這個Activity實例了,結果就是Activity Leak。Android的組件中,Activity對象往往是在堆(Java Heap)里占最多內存的,所以系統會優先回收Activity對象,如果有Activity Leak,APP很容易因為內存不夠而OOM。
采用MVP模式,只要在當前的Activity的onDestroy里,分離異步任務對Activity的引用,就能避免 Activity Leak。
MVP 使用
MVP的主要特點就是把Activity里的許多邏輯都抽離到View和Presenter接口中去,并由具體的實現類來完成。
- 創建IPresenter接口,把所有業務邏輯的接口都放在這里,并創建它的實現PresenterCompl(在這里可以方便地查看業務功能,由于接口可以有多種實現所以也方便寫單元測試),IPresenter持有 IView,調用 IView 中的方法
- 創建IView接口,把所有視圖邏輯的接口都放在這里,其實現類是當前的Activity/Fragment
- 由UML圖可以看出,Activity里包含了一個IPresenter,而PresenterCompl里又包含了一個IView并且依賴了Model。Activity里只保留對IPresenter的調用,其它工作全部留到PresenterCompl中實現
- Model并不是必須有的,但是一定會有View和Presenter
DMEO
- 登陸 view 接口
package io.github.xuyushi.androidmvpdemo.Login.view;
/**
* Created by xuyushi on 16/2/28.
*/
public interface ILoginView {
void clearEditText();
void showProgress();
void hideProgress();
void setUsernameError();
void setPasswordError();
String getUsername();
String getPassword();
void loginSuccess();
}登陸Presenter接口
package io.github.xuyushi.androidmvpdemo.Login.presenter;
/**
* Created by xuyushi on 16/2/28.
*/
public interface ILoginPresenter {
void doLogin(String username, String password);
void clear();
void onDestroy();
}實現Presenter接口
package io.github.xuyushi.androidmvpdemo.Login.presenter;
import android.os.Handler;
import io.github.xuyushi.androidmvpdemo.Login.model.User;
import io.github.xuyushi.androidmvpdemo.Login.view.ILoginView;
/**
* Created by xuyushi on 16/2/28.
*/
public class LoginPresenter implements ILoginPresenter {
private ILoginView mLoginView;
private User mUser;
public LoginPresenter(ILoginView loginView) {
this.mLoginView = loginView;
initUser();
}
private void initUser() {
mUser = new User(mLoginView.getUsername(), mLoginView.getPassword());
}
@Override
public void doLogin(String username, String password) {
mLoginView.showProgress();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mLoginView.hideProgress();
int code = mUser.checkUserValidity(mLoginView.getUsername(), mLoginView.getPassword());
if (code == -1) {
mLoginView.setPasswordError();
} else if (code == 0) {
mLoginView.loginSuccess();
}
}
}, 2000);
}
@Override
public void clear() {
mLoginView.clearEditText();
}
@Override
public void onDestroy() {
mLoginView = null;
}
}定義model
package io.github.xuyushi.androidmvpdemo.Login.model;
/**
* Created by xuyushi on 16/2/28.
*/
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 User(String username, String password) {
this.username = username;
this.password = password;
}
public int checkUserValidity(String username, String password) {
if (username == null || password == null ||
username.isEmpty() ||
password.isEmpty()) {
return -1;
}
return 0;
}
}在 Activity 中實現 view接口
package io.github.xuyushi.androidmvpdemo.Login.view;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import butterknife.Bind;
import butterknife.ButterKnife;
import io.github.xuyushi.androidmvpdemo.Login.presenter.ILoginPresenter;
import io.github.xuyushi.androidmvpdemo.Login.presenter.LoginPresenter;
import io.github.xuyushi.androidmvpdemo.R;
public class LoginActivity extends AppCompatActivity
implements ILoginView, View.OnClickListener {
private ILoginPresenter mLoginPresenter;
@Bind(R.id.et_username)
EditText etUsername;
@Bind(R.id.et_passwrod)
EditText etPasswrod;
@Bind(R.id.bt_enter)
Button btEnter;
@Bind(R.id.bt_clear)
Button btClear;
@Bind(R.id.progress)
ProgressBar progress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mLoginPresenter = new LoginPresenter(this);
btEnter.setOnClickListener(this);
btClear.setOnClickListener(this);
}
@Override
public void clearEditText() {
etPasswrod.setText("");
etUsername.setText("");
}
@Override
public void showProgress() {
progress.setVisibility(View.VISIBLE);
}
@Override
public void hideProgress() {
progress.setVisibility(View.GONE);
}
@Override
public void setUsernameError() {
etUsername.setError("username error");
}
@Override
public void setPasswordError() {
etPasswrod.setError("password error");
}
@Override
public String getUsername() {
return etUsername.getText().toString();
}
@Override
public String getPassword() {
return etPasswrod.getText().toString();
}
@Override
public void loginSuccess() {
//start act Main
Toast.makeText(this, "login success", Toast.LENGTH_SHORT);
finish();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bt_clear:
mLoginPresenter.clear();
break;
case R.id.bt_enter:
mLoginPresenter.doLogin(etUsername.getText().toString(),
etPasswrod.getText().toString());
break;
}
}
@Override
protected void onDestroy() {
mLoginPresenter.onDestroy();
super.onDestroy();
}
}