Android 中的MVP 模式

我的更多 Android 博文

Android 中的MVP 模式

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接口中去,并由具體的實現類來完成。

  1. 創建IPresenter接口,把所有業務邏輯的接口都放在這里,并創建它的實現PresenterCompl(在這里可以方便地查看業務功能,由于接口可以有多種實現所以也方便寫單元測試),IPresenter持有 IView,調用 IView 中的方法
  2. 創建IView接口,把所有視圖邏輯的接口都放在這里,其實現類是當前的Activity/Fragment
  3. 由UML圖可以看出,Activity里包含了一個IPresenter,而PresenterCompl里又包含了一個IView并且依賴了Model。Activity里只保留對IPresenter的調用,其它工作全部留到PresenterCompl中實現
  4. 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();
    }
}

源碼地址
https://github.com/xuyushi/AndroidMVPDemo

MVP 的進化 MVVM 模式,見下一篇博文
http://www.lxweimin.com/p/a15d6e68d93f

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,692評論 25 708
  • 最近稍微了解了下MVP架構模式,這篇文章寫得不錯,轉過來mark下:原博客原地址:http://www.jians...
    Stan_Z閱讀 1,206評論 0 8
  • 聲明:作者原創,轉載注明出處。作者:帥氣陳吃蘋果 一、MVC Model:模型,處理業務邏輯。View:視圖,呈現...
    帥氣陳吃蘋果閱讀 1,016評論 0 8
  • 年度閱讀: 《白夜行》 《隨遇而安》 《蔡康永的說話之道》 《我們臺灣這些年》 《大秦帝國》(第一卷) 《老娘駕到...
    龍貓一只閱讀 403評論 0 0
  • 要愛情還是要面包?沒有面包的愛情不會走的很長遠,因為它失去了最基礎的支撐,就像你喜歡學習,可是沒有學費,你的求...
    陌上桃花開閱讀 463評論 0 1