最簡單的 MVP 理解

前言

一個好的軟件總是離不開好的架構,不管是前端后端。
在Android中,我已知的設計模式有:MVC,MVP、MVVM、Clean,其中各自的優(yōu)劣不再這里展開,有需要的自行Google。
這里探討一下MVP,在很多的文章中,都講很多的概念性的東西,時常把人講的云里霧里,對于剛接觸的人,就算是理解了,怎么實際應用都不知道。
因此本文就用最簡單最常見的來介紹MVP,其實架構是一種很活的東西,誰說你必須使用某種模式?誰規(guī)定代碼一定要這么寫才是對的?我認為只有在變化中能不斷適應的,才是王道。難道后來你會了另一種模式,就不能在已有的項目中應用了嗎?
我認為只要是你邏輯清晰,分層合理,你想怎么玩都行,甚至不用任何所謂的模式,注意:前提是分層一定要清晰,層與層之間的界限要清晰明了。

先不管概念,來一段簡單的代碼先

需求:用戶輸入賬號密碼,點擊登錄按鈕進行登錄。

代碼如下:注意,只是作為示范用。有所刪減,看得懂意圖就好。

activity_login.xml:
如圖,xml 的代碼就不貼了,很簡單。

LoginActivity.java:

public class LoginActivity
        extends AppCompatActivity
{
    private EditText etAccount;
    private EditText etPwd;

    @Override
    protected void onCreate(
            @Nullable
                    Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        etAccount = (EditText) findViewById(R.id.et_account);
        etPwd = (EditText) findViewById(R.id.et_pwd);
    }

    // 響應登錄按鈕
    public void onLogin(View view)
    {
        String account = etAccount.getText().toString();
        String pwd = etPwd.getText().toString();

        // TODO 這里省掉了空判斷

        // 發(fā)起請求
        RequestParams params = new RequestParams();
        params.add("account", account);
        params.add("pwd", pwd);
        new AsyncHttpClient().get("url", params, new Login());
    }

    // 登錄請求回調
    private class Login
            extends AsyncHttpResponseHandler
    {
        @Override
        public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
        {
            if (responseBody != null)
            {
                LoginResponse response = JSON.parseObject(new String(responseBody),
                        LoginResponse.class);
                if (response != null)
                {
                    if (response.getStatus() == 0)
                    {
                        Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();

                        // TODO 去到主界面之類的

                        // 然后結束掉登錄
                        finish();
                    }
                    else
                    {
                        Toast.makeText(LoginActivity.this, "登錄失敗," + response.getMsg(),
                                Toast.LENGTH_SHORT).show();
                    }
                }
            }
            else
            {
                onFailure(statusCode, headers, null, null);
            }
        }

        @Override
        public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                              Throwable error)
        {
            Toast.makeText(LoginActivity.this, "登錄失敗,請檢查網(wǎng)絡", Toast.LENGTH_SHORT).show();
        }
    }
}

很簡單吧?就是輸入和發(fā)起登錄。
其中網(wǎng)絡庫使用的是:
android-async-http
JSON解析使用的是:
FastJSON Android版本

分析,以上代碼一共分為多少層?有什么缺陷?

  • UI層(View):界面的顯示,控件的綁定和操作,用戶的輸入和操作,都屬于UI層需要處理的。比如上述代碼中的findViewById,按鈕響應,Toast,跳轉到其他Activity等操作。在Android中,Activity、Fragment都屬于View。
  • 業(yè)務邏輯層(Presenter):登錄請求的發(fā)起,結果的接收和處理,通知UI層界面更新,都屬于業(yè)務邏輯的范圍。比如上述代碼中的請求發(fā)起和JSON解析,判斷等。
  • 數(shù)據(jù)層(Model):去服務器請求數(shù)據(jù),這里不只是云服務器的請求,數(shù)據(jù)庫,文件,智能設備,任何數(shù)據(jù)源,只要是增刪改查的,都屬于數(shù)據(jù)層的工作。比如上述代碼的網(wǎng)絡庫異步請求。

從分析來看,上述一個簡單的需求實際上有三個層的存在,而卻全部寫在View中,對于新手來說,這樣類似的代碼是再正常不過了。
一般來說,簡單的需求,項目小,這樣寫也不會造成什么問題的,但是一旦項目越來越大,并且需求改動也越來越多的時候,就成了一種災難了,比如無休止的復制和粘貼。

舉個例子:
現(xiàn)在項目中加入了啟動頁,要求在啟動頁判斷先前是否已有用戶登錄過,如果有,則取出賬號密碼進行登錄,登錄成功去到主界面,失敗則去到登錄頁;
如果沒有,直接跳轉到登錄頁。

再用上面的寫法,也就是加個啟動頁,然后復制登錄的那段代碼,再改改回調處理的,聽起來好像沒事,但不覺得重復了嗎?

使用MVP模式重寫

先看一張類圖


你肯定會說:什么?一個簡單的功能,居然需要這么多類文件,這不是更加煩瑣,工作量更加大了嗎?
別急,繼續(xù)看。下面我們就按上面分析的來寫。

LoginResponse.java :JSON 解析需要的數(shù)據(jù)類

public class LoginResponse
{
    private int status;
    private String msg;

    ...省略掉 set/get
}

再來看兩個 Base 類:

BasePresenter.java:

/**
 * 所有Presenter的父接口
 */
public interface BasePresenter
{
    // TODO 在這里可以聲明一些Presenter的通用方法
}

BaseView.java:

/**
 * 所有View的父接口
 */
public interface BaseView
{
    // TODO 在這里可以聲明一些View的通用方法
}

不知道定義兩個 Base 是用來干嘛的,沒關系,再來思考關于這個登錄界面的兩個問題:

  • 1.需要我們處理的用戶操作有哪些?
  • 2.界面?顯示相關的,需要我們做的有哪些?

針對以上問題,解答如下:

  • 1.只有登錄需要我們處理,其他的諸如輸入賬號密碼,點擊按鈕這種操作?不需要我們做。至于賬號密碼的空判斷,已經(jīng)包含在登錄這個操作里了。
  • 2.點擊登錄按鈕后,需要顯示進度條登錄成功需要顯示成功或直接去到主頁面之類的,登錄失敗需要隱藏進度條,?提示登錄失敗的原因之類的。

因此我們可以?把這些操作和顯示都歸類到一個地方,稱為契約類(Contract)

LoginContract.java:

/**
 * 登錄契約類,聲明了View和Presenter該有的操作,方便管理
 */
public interface LoginContract
{
    // 定義界面中所有的 UI 狀態(tài)
    interface View
            extends BaseView
    {
        void loginSuccess(); // 登錄成功

        void loginFailure(String msg); // 登錄失敗

        void showLoading(boolean isShowLoading); // 是否顯示加載中
    }

    // 定義了所有的用戶操作
    interface Presenter
            extends BasePresenter
    {
        void login(String account, String pwd); // 登錄
    }
}

定義契約類的目的是方便管理,也能理清你的邏輯。

好了,?以上都是準備工作,實際的 Model、View、Presenter 相關的具體類還沒寫。繼續(xù)看。

Model:LoginRequest.java

public final class LoginRequest
{
    // 單例
    private LoginRequest()
    {
    }

    private static class SingletonHolder
    {
        private static final LoginRequest SINGLETON = new LoginRequest();
    }

    public static LoginRequest getInstance()
    {
        return SingletonHolder.SINGLETON;
    }

    public void login(String account, String pwd, final LoginCallback callback)
    {
        // 發(fā)起請求
        RequestParams params = new RequestParams();
        params.add("account", account);
        params.add("pwd", pwd);
        new AsyncHttpClient().get("url", params, new AsyncHttpResponseHandler()
        {
            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
            {
                callback.onSuccess(statusCode, responseBody);
            }

            @Override
            public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                                  Throwable error)
            {
                callback.onFailure(statusCode, responseBody, error);
            }
        });
    }

    // 對外暴露的接口
    public interface LoginCallback
    {
        void onFailure(int statusCode, byte[] responseBody, Throwable error);

        void onSuccess(int statusCode, byte[] responseBody);
    }
}

Model 類不負責邏輯的處理,只是負責增刪改查,以及必要的保存住自己的狀態(tài),比如你這個 Model 表示一個智能開關設備,那么開關的狀態(tài)你得保存起來,以便?狀態(tài)改變的時候發(fā)出通知,以及外面的人來你這拿狀態(tài)的時候,你得給人家正確的狀態(tài)。
這里的 Model 表示登錄,login 方法被調用后,將登錄結果通過 LoginCallback 回傳給調用者就完成了職責。

再來看 View。

View:?LoginActivity.java:

public class LoginActivity
        extends AppCompatActivity
        implements LoginContract.View // 實現(xiàn)了?契約類中的接口
{
    private EditText etAccount;
    private EditText etPwd;

    private LoginContract.Presenter loginPresenter;

    @Override
    protected void onCreate(
            @Nullable
                    Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // 創(chuàng)建Presenter,使View和Presenter,Presenter和Model關聯(lián)起來,這一步暫且忽略也可以,等看到 Presenter 了再回來看。
        loginPresenter = new LoginPresenter(this, LoginRequest.getInstance());

        etAccount = (EditText) findViewById(R.id.et_account);
        etPwd = (EditText) findViewById(R.id.et_pwd);
    }

    public void onLogin(View view)
    {
        String account = etAccount.getText().toString();
        String pwd = etPwd.getText().toString();

        // 告知Presenter發(fā)起登錄
        loginPresenter.login(account, pwd);
    }

    @Override
    public void showLoading(boolean isShowLoading)
    {
        // 顯示和隱藏進度條
    }

    @Override
    public void loginSuccess()
    {
        /*
        比如取消進度條,進入到主頁面
         */
    }

    @Override
    public void loginFailure(String msg)
    {
        /*
        取消進度條,顯示登錄錯誤提示,比如密碼錯誤、賬號不存在之類的
         */
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

可以看到,View 中沒有任何的邏輯處理和數(shù)據(jù)獲取,能做的??只是跟界面相關的操作,向?外界發(fā)出請求,以及?暴露給外界操作界面的方法。注意:View 中不能有任何的業(yè)務邏輯處理,只能有和 View 相關的操作。

可以看到,Model 和 View 是完全分開的,沒有任何的直接關聯(lián)。下一步,我們需要通過 Presenter 將他們關聯(lián)起來。

Presenter:LoginPresenter.java

public class LoginPresenter
        implements LoginContract.Presenter
{
    // Presenter ?持有 View 和 Model 的引用
    private LoginContract.View loginView;
    private LoginRequest loginRequest;

    public LoginPresenter(LoginContract.View loginView, LoginRequest loginRequest)
    {
        this.loginView = loginView;
        this.loginRequest = loginRequest;
    }

    @Override
    public void login(String account, String pwd)
    {
        // 賬號密碼不對的話,直接失敗
        if (TextUtils.isEmpty(account.trim()) || TextUtils.isEmpty(pwd))
        {
            loginView.showLoading(false);
            loginView.loginFailure("賬號密碼不對");
            return;
        }

        loginView.showLoading(true);

        loginRequest.login(account, pwd, new LoginRequest.LoginCallback()
        {
            @Override
            public void onFailure(int statusCode, byte[] responseBody, Throwable error)
            {
                loginView.loginFailure("登錄錯誤的提示信息");
            }

            @Override
            public void onSuccess(int statusCode, byte[] responseBody)
            {
                if (responseBody != null)
                {
                    LoginResponse response = JSON.parseObject(new String(responseBody),
                            LoginResponse.class);
                    if (response != null)
                    {
                        if (response.getStatus() == 0)
                        {
                            loginView.loginSuccess();
                        }
                        else
                        {
                            loginView.loginFailure("登錄錯誤的提示信息");
                        }
                    }
                    else
                    {
                        loginView.loginFailure("登錄錯誤的提示信息");
                    }
                }
                else
                {
                    loginView.loginFailure("登錄錯誤的提示信息");
                }
            }
        });
    }
}

可以看到,所有的業(yè)務邏輯都在 Presenter 里面了。

看看以上的代碼是不是符合下面這張圖:

Model 和 View 是完全分離的,以上通過小實例目的是為了讓大家理解并用起來,更復雜徹底的 MVP ,可以查看 Google 的官方 Sample:
googlesamples/android-architecture

還有一個開源項目:
android10/Android-CleanArchitecture

MVP 的好處

  • Model 只有一個,View 只有一個,而 Presenter 可以有多個,但是一個 View 至少對應一個 Presenter,還是那句話,架構是很靈活的,你都把 Model 和 View 分開了,低耦合已經(jīng)實現(xiàn)了,怎么關聯(lián)他們,你看著辦咯。

  • 設計圖出來了,接口還沒好,你可以專注先寫 View,完全不用管數(shù)據(jù)。設計圖沒好,接口好了,你可以先寫 Model,測試接口是否正常,完全不用管 View 是如何設計的。等到都設計好了,?你再把 Model 和 View 關聯(lián)起來專注寫邏輯。是不是覺得無比的清爽?

  • 非常適合于大型的項目,但是要避免過度設計和正確的抽象。

MVP 的壞處

  • 類爆炸

結語:

本文的目的是讓沒有玩過 MVP 的設計快速入門的,?理解了以上內容,?進階的內容可自己 Google,很多這方面的資料。

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

推薦閱讀更多精彩內容