MVP模式


Android MVP Pattern

Android MVP 模式1 也不是什么新鮮的東西了,我在自己的項目里也普遍地使用了這個設計模式。當項目越來越龐大、復雜,參與的研發人員越來越多的時候,MVP 模式的優勢就充分顯示出來了。

導讀:MVP模式是MVC模式在Android上的一種變體,要介紹MVP就得先介紹MVC。在MVC模式中,Activity應該是屬於View這一層。而實質上,它既承擔了View,同時也包含一些Controller的東西在里面。這對於開發與維護來說不太友好,耦合度大高了。把Activity的View和Controller抽離出來就變成了View和Presenter,這就是MVP模式。

基本信息

MVP模式(Model-View-Presenter)可以說是MVC模式(Model-View-Controller)在Android開發上的一種變種、進化模式。后者大家可能比較熟悉,就算不熟悉也可能或多或少地在自己的項目中用到過。要介紹MVP模式,就不得不先說說MVC模式。

MVC模式

MVC模式的結構分為三部分,實體層的Model,視圖層的View,以及控制層的Controller。

  • 其中View層其實就是程序的UI界面,用於向用戶展示數據以及接收用戶的輸入

  • 而Model層就是JavaBean實體類,用於保存實例數據

  • Controller控制器用於更新UI界面和數據實例

例如,View層接受用戶的輸入,然后通過Controller修改對應的Model實例;同時,當Model實例的數據發生變化的時候,需要修改UI界面,可以通過Controller更新界面。(View層也可以直接更新Model實例的數據,而不用每次都通過Controller,這樣對於一些簡單的數據更新工作會變得方便許多。)

舉個簡單的例子,現在要實現一個飄雪的動態壁紙,可以給雪花定義一個實體類Snow,里面存放XY軸坐標數據,View層當然就是SurfaceView(或者其他視圖),為了實現雪花飄的效果,可以啟動一個后台線程,在線程里不斷更新Snow實例里的坐標值,這部分就是Controller的工作了,Controller里還要定時更新SurfaceView上面的雪花。進一步的話,可以在SurfaceView上監聽用戶的點擊,如果用戶點擊,只通過Controller對觸摸點周圍的Snow的坐標值進行調整,從而實現雪花在用戶點擊后出現彈開等效果。具體的MVC模式請自行Google。

MVP模式

在Android項目中,Activity和Fragment占據了大部分的開發工作。如果有一種設計模式(或者說代碼結構)專門是為優化Activity和Fragment的代碼而產生的,你說這種模式重要不?這就是MVP設計模式。

按照MVC的分層,Activity和Fragment(后面只說Activity)應該屬於View層,用於展示UI界面,以及接收用戶的輸入,此外還要承擔一些生命周期的工作。Activity是在Android開發中充當非常重要的角色,特別是TA的生命周期的功能,所以開發的時候我們經常把一些業務邏輯直接寫在Activity里面,這非常直觀方便,代價就是Activity會越來越臃腫,超過1000行代碼是常有的事,而且如果是一些可以通用的業務邏輯(比如用戶登錄),寫在具體的Activity里就意味着這個邏輯不能復用了。如果有進行代碼重構經驗的人,看到1000+行的類肯定會有所顧慮。因此,Activity不僅承擔了View的角色,還承擔了一部分的Controller角色,這樣一來V和C就耦合在一起了,雖然這樣寫方便,但是如果業務調整的話,要維護起來就難了,而且在一個臃腫的Activity類查找業務邏輯的代碼也會非常蛋疼,所以看起來有必要在Activity中,把View和Controller抽離開來,而這就是MVP模式的工作了。

MVP模式的核心思想:

MVP把Activity中的UI邏輯抽象成View接口,把業務邏輯抽象成Presenter接口,Model類還是原來的Model

這就是MVP模式,現在這樣的話,Activity的工作的簡單了,只用來響應生命周期,其他工作都丟到Presenter中去完成。從上圖可以看出,Presenter是Model和View之間的橋梁,為了讓結構變得更加簡單,View並不能直接對Model進行操作,這也是MVP與MVC最大的不同之處。

MVP模式的作用

MVP的好處都有啥,誰說對了就給他 KIRA!!(<ゝω·)☆

  • 分離了視圖邏輯和業務邏輯,降低了耦合

  • Activity只處理生命周期的任務,代碼變得更加簡潔

  • 視圖邏輯和業務邏輯分別抽象到了View和Presenter的接口中去,提高代碼的可閱讀性

  • Presenter被抽象成接口,可以有多種具體的實現,所以方便進行單元測試

  • 把業務邏輯抽到Presenter中去,避免后台線程引用着Activity導致Activity的資源無法被系統回收從而引起內存泄露和OOM

其中最重要的有三點:

Activity 代碼變得更加簡潔

相信很多人閱讀代碼的時候,都是從Activity開始的,對着一個1000+行代碼的Activity,看了都覺得難受。

使用MVP之后,Activity就能瘦身許多了,基本上只有FindView、SetListener以及Init的代碼。其他的就是對Presenter的調用,還有對View接口的實現。這種情形下閱讀代碼就容易多了,而且你只要看Presenter的接口,就能明白這個模塊都有哪些業務,很快就能定位到具體代碼。Activity變得容易看懂,容易維護,以后要調整業務、刪減功能也就變得簡單許多。

方便進行單元測試

一般單元測試都是用來測試某些新加的業務邏輯有沒有問題,如果采用傳統的代碼風格(習慣性上叫做MV模式,少了P),我們可能要先在Activity里寫一段測試代碼,測試完了再把測試代碼刪掉換成正式代碼,這時如果發現業務有問題又得換回測試代碼,咦,測試代碼已經刪掉了!好吧重新寫吧……

MVP中,由於業務邏輯都在Presenter里,我們完全可以寫一個PresenterTest的實現類繼承Presenter的接口,現在只要在Activity里把Presenter的創建換成PresenterTest,就能進行單元測試了,測試完再換回來即可。萬一發現還得進行測試,那就再換成PresenterTest吧。

避免 Activity 的內存泄露

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模式的UML圖,從圖中可以看出,使用MVP,至少需要經歷以下步驟:

  1. 創建IPresenter接口,把所有業務邏輯的接口都放在這里,並創建它的實現PresenterCompl(在這里可以方便地查看業務功能,由於接口可以有多種實現所以也方便寫單元測試)

  2. 創建IView接口,把所有視圖邏輯的接口都放在這里,其實現類是當前的Activity/Fragment

  3. 由UML圖可以看出,Activity里包含了一個IPresenter,而PresenterCompl里又包含了一個IView並且依賴了Model。Activity里只保留對IPresenter的調用,其它工作全部留到PresenterCompl中實現

  4. Model並不是必須有的,但是一定會有View和Presenter

通過上面的介紹,MVP的主要特點就是把Activity里的許多邏輯都抽離到View和Presenter接口中去,並由具體的實現類來完成。這種寫法多了許多IView和IPresenter的接口,在某種程度上加大了開發的工作量,剛開始使用MVP的小伙伴可能會覺得這種寫法比較別扭,而且難以記住。其實一開始想太多也沒有什么卵用,只要在具體項目中多寫幾次,就能熟悉MVP模式的寫法,理解TA的意圖,以及享♂受其帶來的好處。

扯了這么多,但是好像並沒有什么卵用,畢竟

Talk is cheap, let me show you the code!

所以還是來寫一下實際的項目吧。

MVP模式簡單實例

一個簡單的登錄界面(實在想不到別的了╮( ̄▽ ̄")╭),點擊LOGIN則進行賬號密碼驗證,點擊CLEAR則重置輸入。

項目結構看起來像是這個樣子的,MVP的分層還是很清晰的。我的習慣是先按模塊分Package,在模塊下面再去創建model、view、presenter的子Package,當然也可以用model、view、presenter作為頂級的Package,然后把所有的模塊的model、view、presenter類都到這三個頂級Package中,就好像有人喜歡把項目里所有的Activity、Fragment、Adapter都放在一起一樣。

首先來看看LoginActivity

public class LoginActivity extends ActionBarActivity implements ILoginView, View.OnClickListener { private EditText editUser; private EditText editPass; private Button btnLogin; private Button btnClear; ILoginPresenter loginPresenter; private ProgressBar progressBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //find view editUser = (EditText) this.findViewById(R.id.et_login_username); editPass = (EditText) this.findViewById(R.id.et_login_password); btnLogin = (Button) this.findViewById(R.id.btn_login_login); btnClear = (Button) this.findViewById(R.id.btn_login_clear); progressBar = (ProgressBar) this.findViewById(R.id.progress_login); //set listener btnLogin.setOnClickListener(this); btnClear.setOnClickListener(this); //init loginPresenter = new LoginPresenterCompl(this); loginPresenter.setProgressBarVisiblity(View.INVISIBLE); } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_login_clear: loginPresenter.clear(); break; case R.id.btn_login_login: loginPresenter.setProgressBarVisiblity(View.VISIBLE); btnLogin.setEnabled(false); btnClear.setEnabled(false); loginPresenter.doLogin(editUser.getText().toString(), editPass.getText().toString()); break; } } @Override public void onClearText() { editUser.setText(""); editPass.setText(""); } @Override public void onLoginResult(Boolean result, int code) { loginPresenter.setProgressBarVisiblity(View.INVISIBLE); btnLogin.setEnabled(true); btnClear.setEnabled(true); if (result){ Toast.makeText(this,"Login Success",Toast.LENGTH_SHORT).show(); startActivity(new Intent(this, HomeActivity.class)); } else Toast.makeText(this,"Login Fail, code = " + code,Toast.LENGTH_SHORT).show(); } @Override public void onSetProgressBarVisibility(int visibility) { progressBar.setVisibility(visibility); } }

從代碼可以看出LoginActivity只做了findView以及setListener的工作,而且包含了一個ILoginPresenter,所有業務邏輯都是通過調用ILoginPresenter的具體接口來完成。所以LoginActivity的代碼看起來很舒爽,甚至有點愉♂悅呢 (/ω\*)。視力不錯的你可能還看到了ILoginView接口的實現,如果不懂為什么要這樣寫的話,可以先往下看,這里只要記住LoginActivity實現了ILoginView接口

再來看看ILoginPresenter

public interface ILoginPresenter { void clear(); void doLogin(String name, String passwd); void setProgressBarVisiblity(int visiblity); }
public class LoginPresenterCompl implements ILoginPresenter { ILoginView iLoginView; IUser user; Handler handler; public LoginPresenterCompl(ILoginView iLoginView) { this.iLoginView = iLoginView; initUser(); handler = new Handler(Looper.getMainLooper()); } @Override public void clear() { iLoginView.onClearText(); } @Override public void doLogin(String name, String passwd) { Boolean isLoginSuccess = true; final int code = user.checkUserValidity(name,passwd); if (code!=0) isLoginSuccess = false; final Boolean result = isLoginSuccess; handler.postDelayed(new Runnable() { @Override public void run() { iLoginView.onLoginResult(result, code); } }, 3000); } @Override public void setProgressBarVisiblity(int visiblity){ iLoginView.onSetProgressBarVisibility(visiblity); } private void initUser(){ user = new UserModel("mvp","mvp"); } }

從代碼可以看出,LoginPresenterCompl保留了ILoginView的引用,因此在LoginPresenterCompl里就可以直接進行UI操作了,而不用在Activity里完成。這里使用了ILoginView引用,而不是直接使用Activity,這樣一來,如果在別的Activity里也需要用到相同的業務邏輯,就可以直接復用LoginPresenterCompl類了(一個Activity可以包含一個以上的Presenter,總之,需要什么業務就new什么樣的Presenter,是不是很靈活(@ ̄︶ ̄@)),這也是MVP的核心思想

通過IVIew和IPresenter,把Activity的UI LogicBusiness Logic分離開來,Activity just does its basic job! 至於Model嘛,還是原來MVC里的Model。

再來看看ILoginView,至於ILoginView的實現類呢,翻到上面看看LoginActivity吧

public interface ILoginView { public void onClearText(); public void onLoginResult(Boolean result, int code); public void onSetProgressBarVisibility(int visibility); }

代碼這種東西放在日志里講好像除了把整個版面拉長沒什么卵用,我把幾種自己常用的MVP的寫法寫成一個Demo項目,歡迎圍觀和PullRequest:Android-MVP-Pattern

后記

以上就是我的MVP模式的一點理解,在MVVM模式還沒有成熟的現在,我覺得沒有比MVP模式更好的替代品了。當然今天寫的只是MVP的基礎使用,介紹的實例項目也非常簡單,看不出MVP的優勢,后面還會針對MVP模式寫一些日志,就目前能想到的至少包括

  • Android常規的開發模式經常被稱為MV模式(Model-View),引入數據綁定后的MVVM模式(Model-View-ViewModel),與MVP模式的區別

  • 目前我們寫ListView的Adapter都喜歡把它寫成一個內部類,如果有兩個Activity里要用同一個Adapter就比較難了,通過MVP模式,能輕松地復用Adapter(你說已經不用ListView了,這不是重點不是么( ˃◡˂ ))

  • MVP模式需要多寫許多新的接口,這也是其缺點所在,經過一段時間的實戰,我自己已有一種優化的MVP模式,我會試着總結一下,把她拿出來說說


  1. 我也糾結過MVP模式或者MVP結構的說法那個跟准確一點,國外普遍的叫法是直接叫Android MVP,除此之外有叫MVP Pattern的也有叫MVP Framework/Architecture,個人認為這應該算是一種代碼風格(Code Style),在分類上應該比較類似設計模式(Design Pattern),所以現在我一般稱為模式,不過這不是重點,不是嗎。( ˃◡˂ ) 

//-----------------------------------------下面是自己的一個Demo,實現的是登錄頁面---------------------------------------------------------

 

//------------------Xml 布局中--------------------------------
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical"
>


<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_username"
android:hint="請輸入用戶名"
android:layout_gravity="center_horizontal" />

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="請輸入用戶名"
android:id="@+id/et_password"
android:layout_gravity="center_horizontal" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登錄"
android:id="@+id/button"
android:layout_gravity="center_horizontal" />
</LinearLayout>

//-----------------MainActivity中----------------------------
package mvpmodledemo1.wode.com.mvpmodledemo1;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import mvpmodledemo1.wode.com.mvpmodledemo1.presenter.IPresenter;
import mvpmodledemo1.wode.com.mvpmodledemo1.presenter.IpresenterCompl;
import mvpmodledemo1.wode.com.mvpmodledemo1.view.ILoginView;

//實現ILoginView接口
public class MainActivity extends AppCompatActivity implements View.OnClickListener,ILoginView{

private EditText et_password;
private EditText et_username;
private Button button;

private IPresenter iPresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化空件
initView();
//調用邏輯層
iPresenter=new IpresenterCompl(this);
}
//初始化空件
private void initView() {
et_password = (EditText) findViewById(R.id.et_password);
et_username = (EditText) findViewById(R.id.et_username);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);


}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.button:
String use=et_username.getText().toString().trim();
String pwdstr=et_password.getText().toString().trim();
//判斷輸入內容不能為空
if (!TextUtils.isEmpty(use)&&!TextUtils.isEmpty(pwdstr)){
//調用邏輯層的登錄
iPresenter.login(use,pwdstr);
}else{

}
break;

}
}

/**
* 實現ILoginView接口中的方法
* @param ret
* @param code
*/
@Override
public void LoginResult(boolean ret, String code) {
Toast.makeText(this,"登陸結果"+ret+code,Toast.LENGTH_SHORT).show();;

}
}

//------------------ IUser類中----------------Modle層-------------------
public interface IUser {
public String getUsername();
public String getPWD();
public boolean checkLoginVisible(String username,String pwd);
}

//-------------------UserModle類中---------------Modle層-----------------
public class UserModle implements IUser{
private String username;
private String pwd;

public UserModle(String username, String pwd) {
this.username = username;
this.pwd = pwd;
}

@Override
public String getUsername() {
return username;
}

@Override
public String getPWD() {
return pwd;
}

@Override
public boolean checkLoginVisible(String username, String pwd) {
if (this.username.equals(username)&&this.pwd.equals(pwd)){
return true;
}
return false;
}
}
//---------------------IPresenter類中-------------------Presenter(邏輯層)-----------
public interface IPresenter {
public void login(String user,String pwd);
}

//-------------------IpresenterCompl類中-----------Presenter(邏輯層)-----------
public class IpresenterCompl implements IPresenter{
private ILoginView iLoginView;
private IUser iUser;

public IpresenterCompl(ILoginView iLoginView) {
this.iLoginView = iLoginView;
//初始化數據
initUser();
}
//初始化數據
private void initUser() {
iUser=new UserModle("username","pwd");

}

@Override
public void login(String user, String pwd) {
boolean ret=iUser.checkLoginVisible(user,pwd);
iLoginView.LoginResult(ret,"0");

}

//-----------------ILoginView-----------View層------------------
public interface ILoginView {
public void LoginResult(boolean ret,String code);
}
 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM