從我們的項目到JD,處處可見Android MVP 模式的身影。
項目越來越龐大,開發越來越多,MVP的優勢越來越明顯。
今天我們通過一個簡單的登錄Demo,初步學會MVP的使用。
首先,什麼是MVP?
MVP模式是MVC模式在Android上的一種變體,要介紹MVP就得先介紹MVC。
在MVC模式中,Activity應該是屬於View這一層。而實質上,它既承擔了View,同時也包含一些Controller的東西。因爲耦合度高,因此不利於我們的開發與維護。
所以我們把Activity的View和Controller抽離出來就變成了View和Presenter,這就是MVP模式。
MVP模式的核心思想:
這樣,Activity的工作簡單了,只用來響應生命週期,其他工作都由Presenter去完成。從上圖可以看出,Presenter是Model和View之間的橋樑,爲了讓結構變得更加簡單,View並不能直接對Model進行操作,這也是MVP與MVC最大的不同之處。MVP把Activity中的UI邏輯抽象成View接口,把業務邏輯抽象成Presenter接口,Model不變。
MVP的優勢:
-
分離了視圖邏輯和業務邏輯,降低了耦合
-
Activity只處理生命週期的任務,代碼變得更加簡潔
-
視圖邏輯和業務邏輯分別抽象到了View和Presenter的接口中去,提高代碼的可閱讀性
-
Presenter被抽象成接口,可以有多種具體的實現,所以方便進行單元測試
-
把業務邏輯抽到Presenter中去,避免後臺線程引用Activity導致Activity的資源無法被系統回收從而引起內存泄露和OOM
使用MVP之後,Activity就能瘦身許多了,基本上只有FindView、SetListener以及Init的代碼。其他的就是對Presenter的調用,還有對View接口的實現。這種情形下閱讀代碼就容易多了,而且你只要看Presenter的接口,就能明白這個模塊都有哪些業務,很快就能定位到具體代碼。Activity變得容易看懂,容易維護,以後要調整業務、刪減功能也就變得簡單許多。
一般單元測試都是用來測試某些新加的業務邏輯有沒有問題,如果採用傳統的代碼風格(習慣性上叫做MV模式,少了P),我們可能要先在Activity裏寫一段測試代碼,測試完了再把測試代碼刪掉換成正式代碼,這時如果發現業務有問題又得換回測試代碼,咦,測試代碼已經刪掉了!好吧重新寫吧……MVP中,由於業務邏輯都在Presenter裏,我們完全可以寫一個PresenterTest的實現類繼承Presenter的接口,現在只要在Activity裏把Presenter的創建換成PresenterTest,就能進行單元測試了,測試完再換回來即可。萬一發現還得進行測試,那就再換成PresenterTest吧。
Android APP 發生OOM的最大原因就是出現內存泄露造成APP的內存不夠用,而造成內存泄露的兩大原因之一就是Activity泄露(Activity Leak)(另一個原因是Bitmap泄露(Bitmap Leak))。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。
說了那麼多,開始實戰,先看戰略地圖(UML圖,PPT純手繪):
-
創建IPresenter接口,把所有業務邏輯的接口都放在這裏,並創建它的實現PresenterImpl(在這裏可以方便地查看業務功能,由於接口可以有多種實現所以也方便寫單元測試)
-
創建IView接口,把所有視圖邏輯的接口都放在這裏,其實現類是當前的Activity/Fragment
-
由UML圖可以看出,Activity裏包含了一個IPresenter,而PresenterImpl裏又包含了一個IView並且依賴了Model。Activity裏只保留對IPresenter的調用,其它工作全部留到PresenterImpl中實現
-
Model並不是必須有的,但是一定會有View和Presenter
通過上面的介紹,MVP的主要特點就是把Activity裏的許多邏輯都抽離到View和Presenter接口中去,並由具體的實現類來完成。這種寫法多了許多IView和IPresenter的接口,在某種程度上加大了開發的工作量,剛開始使用MVP的小夥伴可能會覺得這種寫法比較彆扭,而且難以記住。其實一開始想太多也沒有什麼卵用,只要在具體項目中多寫幾次,就能熟悉MVP模式的寫法,理解TA的意圖,以及享受其帶來的好處。
PS:
注意一下UML圖中的關係,相互依賴(虛線箭頭),相互關聯(實線箭頭)。
依賴表現爲函數中的參數(use a),是類與類之間的連接,表示一個類依賴於另一個類的定義,其中一個類的變化將影響另外一個類。例如如果A依賴於B,則B體現爲局部變量,方法的參數、或靜態方法的調用。如電視(TV)依賴於頻道(channel)常見的依賴關係如下:
(1)類B以參數的形式傳入類A的方法。
(2)類B以局部變量的形式存在於類A的方法中。
(3)類A調用類B的靜態方法。
關聯關係包括單項關聯,雙向關聯,組合和聚合
分析依賴和關聯的關係
1.依賴是一種弱關聯
2.依賴關係表現在局部變量,方法的參數,以及對靜態方法的調用
3.關聯關係中,體現的是兩個類、或者類與接口之間語義級別的一種強依賴關係,比如我和我的朋友;這種關係比依賴更強、不存在依賴關係的偶然性、關係也不是臨時性的,一般是長期性的,而且雙方的關係一般是平等的。
4.依賴關係中,可以簡單的理解,就是一個類A使用到了另一個類B,而這種使用關係是具有偶然性的、臨時性的、非常弱的,但是B類的變化會影響到A。
開始Coding,一個登錄小Demo。
先看一下項目結構:
從V開始,先看接口ILoginView,定義實現類需要實現的方法,清除文本和登錄結果展示。
package com.example.quan.quanstudy.MVP_login.view;
/**
* Created by xingquan.he on 2017/3/7.
*/
public interface ILoginView {
public void onClearText();
public void onLoginResult(Boolean result, int code);
}
然後就是實現類LoginActivity,在這裏操作UI,關聯P(成員變量),實現接口方法,然後就是初始化View和Listener,業務邏輯都通過ILoginPresenter的具體實現類完成,代碼很清爽有木有~
package com.example.quan.quanstudy.MVP_login.view;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.example.quan.quanstudy.MVP_login.presenter.ILoginPresenter;
import com.example.quan.quanstudy.MVP_login.presenter.LoginPresenterImpl;
import com.example.quan.quanstudy.R;
/**
* Created by xingquan.he on 2017/3/7.
*/
public class LoginActivity extends Activity implements ILoginView, View.OnClickListener {
private EditText mEditUser;
private EditText mEditPass;
private Button mBtnLogin;
private Button mBtnClear;
private ProgressBar mProgressBarLogin;
private ILoginPresenter mLoginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp_login);
initView();
initOnClickListener();
mLoginPresenter = new LoginPresenterImpl(this);
mProgressBarLogin.setVisibility(View.INVISIBLE);
}
private void initView() {
mEditUser = (EditText) this.findViewById(R.id.et_login_username);
mEditPass = (EditText) this.findViewById(R.id.et_login_password);
mBtnLogin = (Button) this.findViewById(R.id.btn_login_login);
mBtnClear = (Button) this.findViewById(R.id.btn_login_clear);
mProgressBarLogin = (ProgressBar) this.findViewById(R.id.progressbar_login);
}
private void initOnClickListener() {
mBtnLogin.setOnClickListener(this);
mBtnClear.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_login_clear:
mLoginPresenter.clear();
break;
case R.id.btn_login_login:
mProgressBarLogin.setVisibility(View.VISIBLE);
mBtnLogin.setEnabled(false);
mBtnClear.setEnabled(false);
mLoginPresenter.doLogin(mEditUser.getText().toString(), mEditPass.getText().toString());
break;
}
}
@Override
public void onClearText() {
mEditUser.setText("");
mEditPass.setText("");
}
@Override
public void onLoginResult(Boolean result, int code) {
mProgressBarLogin.setVisibility(View.INVISIBLE);
mBtnLogin.setEnabled(true);
mBtnClear.setEnabled(true);
if (result) {
Toast.makeText(this,"Login Success",Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(this,"Login Fail, code = " + code,Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
接下來我們看P,先看接口,定義清除和登錄方法:
package com.example.quan.quanstudy.MVP_login.presenter;
/**
* Created by xingquan.he on 2017/3/7.
*/
public interface ILoginPresenter {
void clear();
void doLogin(String name, String passwd);
}
具體的P實現,V(在P而不是Activity裏操作UI,簡化邏輯,如果在別的 Activity 裏也需要用到相同的業務邏輯,就可以直接複用 P(一個V可以包含一個以上的 P,總之,需要什麼業務就 new 什麼樣的 Presenter,靈活~)和M的引用作爲成員變量,關聯V依賴M,構造方法中初始化賦值:
private ILoginView mLoginView;
private User mUser;
private Handler mHandler;//模擬登錄耗時操作
public LoginPresenterImpl(ILoginView iLoginView) {
this.mLoginView = iLoginView;
mUser = new User("xq.he","mvp");
mHandler = new Handler(Looper.getMainLooper());
}
複寫接口定義的方法:
@Override
public void clear() {
mLoginView.onClearText();
}
@Override
public void doLogin(String name, String password) {
Boolean isLoginSuccess = true;
final int code = mUser.checkUserValidity(name,password);
if (code != 0) {
isLoginSuccess = false;
}
final Boolean result = isLoginSuccess;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mLoginView.onLoginResult(result, code);
}
}, 4000);
}
這樣,我們就通過 IView 和 IPresenter,把 MVC中Activity的V和C分離開來,M不變~
package com.example.quan.quanstudy.MVP_login.model;
/**
* Created by xingquan.he on 2017/3/7.
*/
public class User {
private String mName;
private String mPassword;
public User(String name, String password) {
this.mName = name;
this.mPassword = password;
}
public String getName() {
return mName;
}
public String getPassword() {
return mPassword;
}
public int checkUserValidity(String name, String password) {
if ( name == null || password == null || !name.equals(getName()) || !password.equals(getPassword()) ){
return -1;
}
return 0;
}
}
完成後界面如圖,十分簡單的一個登錄Demo,關鍵是領會MVP。
原創不易,轉載請註明出處哈。
權興權意
代碼可以更優雅~
http://blog.csdn.net/hxqneuq2012/article/details/60870945
項目源代碼,歡迎提建議(star)。
https://github.com/HXQWill/QuanStudy/tree/master/app/src/main/java/com/example/quan/quanstudy/MVP_login
參考:
MVP 模式簡單易懂的介紹方式(大讚~,本文在此基礎上修改了P和V的部分邏輯和代碼規範)
http://kaedea.com/2015/10/11/android-mvp-pattern/
UML類圖實例(重要的基礎課)
http://blog.csdn.net/xhf55555/article/details/6896316/