一個登錄Demo初識MVP

從我們的項目到JD,處處可見Android MVP 模式的身影。

項目越來越龐大,開發越來越多,MVP的優勢越來越明顯。

今天我們通過一個簡單的登錄Demo,初步學會MVP的使用。


首先,什麼是MVP?

MVP模式是MVC模式在Android上的一種變體,要介紹MVP就得先介紹MVC。

在MVC模式中,Activity應該是屬於View這一層。而實質上,它既承擔了View,同時也包含一些Controller的東西。因爲耦合度高,因此不利於我們的開發與維護。

所以我們把Activity的View和Controller抽離出來就變成了View和Presenter,這就是MVP模式。

MVP模式的核心思想:

MVP把Activity中的UI邏輯抽象成View接口,把業務邏輯抽象成Presenter接口,Model不變

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


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純手繪):


上面一張簡單的MVP模式的UML圖,從圖中可以看出,使用MVP,至少需要經歷以下步驟:

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

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

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

  4. 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/

發佈了166 篇原創文章 · 獲贊 16 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章