Android MVP模式從入門到進門(一)

蘆葦科技App技術團隊發表於2018-12-29

今天是2018年最後一個工作日了,提前祝大家新年快樂啦~~~

這是一篇面向Android初學者拋磚引玉的文章,正如以前的我——寫程式碼只考慮如何實現功能,對於設計模式完全沒有想法和認知。在這篇文章中,我會通過一個常用的登入場景,從幾十行程式碼的直接實現,一步步構建出入門級的MVP架構,向你們分享我所理解的程式碼的流暢性。但限於文章長度,本篇先對實現MVP前我認為需要了解的一些程式碼優化內容做介紹,比如為什麼要用到介面,以及程式碼的流暢性等。

當然,書讀千遍不如行萬里路,真正地理解,一定是在自己不斷敲程式碼的過程中獲得的。這是我切身感受到的,也推薦如果是剛入門的你這樣去做:先按照網上的示例去“模仿”實現,在做過多次後,那些理念性的優缺點自然就能感受並理解了。

這次使用一個常用的手機號+驗證碼的登入場景作為示例,看一下效果圖吧:

Android MVP模式從入門到進門(一)

首先在不使用MVC或者MVP等設計模式的情況下,看下如何手擼出上面的效果:

public class LoginAcitvity extends AppCompatActivity {

    @BindView(R.id.et_phone)
    EditText mEtPhone;
    @BindView(R.id.et_code)
    EditText mEtCode;
    @BindView(R.id.pb_loading)
    ProgressBar mPbLoading;
    private String mRandomCode;

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

    /**
     * 點選獲取驗證碼 生成6位隨機數並顯示
     */
    private void showCode() {
        //建立隨機驗證碼
        Random random = new Random();
        StringBuilder rCode = new StringBuilder();
        int codeMaxLength = 6;
        for (int i = 0; i < codeMaxLength; i++) {
            rCode.append(random.nextInt(10));
        }
        mRandomCode = rCode.toString();
        //將建立的驗證碼顯示出來
        Toast.makeText(this, "驗證碼:" + mRandomCode, Toast.LENGTH_SHORT).show();
    }

    /**
     * 驗證登入
     */
    private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        //用ProgressBar作為Loading控制元件,在驗證登入前顯示
        mPbLoading.setVisibility(View.VISIBLE);
        //用handler的延遲操作模擬網路效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //無論登入成功與否,都關掉loading控制元件的顯示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入失敗" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
    }

    @OnClick({R.id.btn_code, R.id.btn_login})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_code:
                showCode();
                break;
            case R.id.btn_login:
                login();
                break;
            default:
        }
    }
}
複製程式碼

程式碼中使用了 Butterknife 代替 findViewById 實現對 View 的繫結和 Click 的事件處理。 其中主要包含兩個方法:

  1. void showCode()

    點選獲取驗證碼按鈕時呼叫,因為是測試環境,所以直接生成6位隨機數作為驗證碼並顯示出來,同時傳入全域性變數mRandomCode中以作登入校驗用。

  2. void login()

    點選登入按鈕時呼叫,校驗輸入的手機號和驗證碼,通過handler的delay操作延遲1秒模擬網路環境。在校驗前顯示loading控制元件,返回結果後隱藏。

噠噠~只用了幾十行程式碼就完整實現了圖中的功能,並且還沒出現bug呢。不過程式碼作為新時代的藝術,我們自然是不能就此滿足了,還有很多優化之路要走。

可能有同學就會問了:“ 這樣寫不是挺好的嗎,一個Activity裡就寫完所有邏輯了,很方便直接啊。”

確實是,在處理一些簡單任務的時候,一行行堆砌程式碼的確來的快捷簡便。但如果程式碼堆疊得多了,Activity就會變得特別臃腫,我們看一下在上面這個簡單的例子中,Activity負責了哪些行為:

  • 對各種控制元件進行繫結和控制
  • 獲取使用者的輸入、點選事件
  • 向伺服器傳送獲取驗證碼的請求(因為是模擬登入,所以只是建立隨機驗證碼並顯示給使用者以模擬這一步驟)
  • 向伺服器傳送手機號和驗證碼,獲取驗證結果(也是模擬驗證)
  • 將結果在頁面上顯示出來告知使用者
  • 管理自身相關生命週期的事務、例如在退出時關閉網路連線等(因為是模擬沒有實際網路連線,所以程式碼中沒有體現)

將這些行為按照如下規則分類:

  • 跟介面相關,負責處理各種介面操作
    • 控制控制元件
    • 獲取事件
    • 生命週期
    • 顯示結果
  • 跟介面無關,負責處理業務的邏輯
    • 向伺服器獲取驗證碼
    • 向伺服器驗證登入

可以發現,如果按照責任劃分,出現了以介面處理和業務處理兩種型別的程式碼行為。那麼這是否可以作為我們優化程式碼流暢性的一個參考標準呢?如果將程式碼按照上面的分類進行改寫,會有怎樣的效果?

我們回看上面void login()部分的程式碼:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
    	//用ProgressBar作為Loading控制元件,在驗證登入前顯示
        mPbLoading.setVisibility(View.VISIBLE);
        //用handler的延遲操作模擬網路效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //無論登入成功與否,都關掉loading控制元件的顯示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入失敗" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
    }
複製程式碼

可以發現其中包含“獲取輸入”、“控制Loading控制元件”、”驗證登入“以及”顯示結果”四個任務,也就是既有對介面的操控,又對伺服器進行資料處理。我們試著把這兩者分開看一下:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

public void verifyLogin(String phone, String code){
    	//用ProgressBar作為Loading控制元件,在驗證登入前顯示
        mPbLoading.setVisibility(View.VISIBLE);
    	//用handler的延遲操作模擬網路效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //無論登入成功與否,都關掉loading控制元件的顯示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登入失敗" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
}
複製程式碼

將驗證登入這一部分獨立出來後,發現其方法裡還是有很多程式碼,我們再將其按照責任分離一下,達到下面這種效果:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

public void verifyLogin(String phone, String code){
    	showLoading();
    	//用handler的延遲操作模擬網路效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                onLoginSuccess();
            } else {
                onLoginFail();
            }
        }, 1000);
}

public void showMessage(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

public void hideLoading() {
        mPbLoading.setVisibility(View.INVISIBLE);
    }

public void onLoginSuccess() {
        hideLoading();
        showMessage("登入成功");
    }

public void onLoginFail() {
        hideLoading();
        showMessage("登入失敗");
    }
複製程式碼

怎樣,是不是感覺程式碼變得“好看”了許多。雖然從一個方法,分而變成了很多個,但我們主要的目的還是按照“介面 - 業務”進行分類,其他void showLoading()void hideLoading()等方法都是為了更方便在程式碼中複用而建立的。

說到程式碼複用,我想到了今年下半年··emm 不開花先。

我們可以思考一下什麼方法是比較通用的,在我看來有以下三個:

  • void showMessage(String msg) 很多地方需要顯示訊息,文中使用了常用的Toast。
  • void showLoading() 需要耗時操作的業務一般都會有Loading控制元件
  • void hideLoading() 有顯示自然就有隱藏

那麼對於這些通用的方法,自然而然我們引入到了介面(Interface)的概念,既然每個Activity都有很大可能用到這些方法,那我們可以宣告一個介面,讓需要用到Activity實現這個介面吧:

public interface IBaseActivity {
    /**
     * 顯示Loading
     */
    void showLoading();

    /**
     * 關閉Loading
     */
    void hideLoading();

    /**
     * 顯示訊息
     * @param msg
     */
    void showMessage(String msg);

}
複製程式碼

其實我看過很多介紹MVP的文章,裡面都有繼承和實驗介面的操作,但往往不會介紹太清楚。如果你像我一樣對JAVA基礎不牢固,在還不甚瞭解介面這部分知識的時候去閱讀這些文章,很容易會不明其所以然。所以我推薦你如果不太瞭解介面和繼承的知識,可以先去閱讀一下相關概念。

此處運用介面的意義在於將通用的方法獨立出來,以供需要它的類直接實現和重寫該方法,我將這種介面叫做通用介面。

但是通用的介面,往往實現的方法不多,如果我想再多實現一些方法呢?我們做個極端一點的例子,將上面LoginActivity中所有方法都寫成介面的形式,程式碼的效果是這樣的(接下來的程式碼都去掉了註釋以縮短文章長度):

public interface InterfaceBase {
    
    void showLoading();

    void hideLoading();

    void showMessage(String msg);

    void sentCode();

    void login();

    void verifyLogin();

    void onLoginSuccess();

    void onLoginFail();
}
複製程式碼

這樣一來,我們直接實現這個介面,就可以省得再去Activity中建立這些方法了。而實際開發中也確實是這樣,因為能直觀地在介面中看到所有的方法,所以我們會在建立Activity前先建立介面,宣告需要實現的一些方法,然後在Activity中實現介面就可以了。

對於這種專司其職的介面,我將其稱為專用介面。

那既然已經有了專用介面,前面提到的通用介面還有什麼用處呢?一個類只能繼承一個介面,我們肯定選擇繼承功能強大的專用介面,而不是方法少、功能單一的通用介面啊。可以看到,上面提供的專用介面中,仍然包含了void showLoading();void hideLoading();void showMessage();這三個通用方法,如果每次建立專用介面都新增這三個方法,肯定不是聰明的選擇。於是介面的繼承就派上用場了——每次建立專用介面時繼承通用介面,這樣就可以更方便地實現所有方法了。

但是,前面說到將所有方法都在介面中宣告出來,是比較極端的方式,一般是不會這樣去寫介面的。其實介面的定義很多,我只是按我理解的方式去設計它而已。

在這裡我只保留了其中關於登入結果回撥的方法,至於原因會在接下來MVP相關內容時講到。下面是繼承了通用介面只保留登入結果回撥的介面程式碼:

public interface InterfaceLogin extends InterfaceBase {
    
    void onLoginSuccess();

    void onLoginFail();
}
複製程式碼

於是對各種方法進行細分、實現繼承完的介面後,我們的Activity就變成了這樣:

public class LoginActivity extends AppCompatActivity implements ILoginActivity {

    @BindView(R.id.et_phone)
    EditText mEtPhone;
    @BindView(R.id.et_code)
    EditText mEtCode;
    @BindView(R.id.pb_loading)
    ProgressBar mPbLoading;
    //生成的隨機6位數驗證碼
    private String mRandomCode;

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

    @Override
    public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        mPbLoading.setVisibility(View.INVISIBLE);
    }

    @Override
    public void showMessage(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }


    public void sentCode() {
        //生成驗證碼
        mRandomCode = generateCode();
        //將建立的驗證碼顯示出來
        showCode();
    }


    private String generateCode() {
        Random random = new Random();
        StringBuilder rCode = new StringBuilder();
        int codeMaxLength = 6;
        for (int i = 0; i < codeMaxLength; i++) {
            rCode.append(random.nextInt(10));
        }
        return rCode.toString();
    }

    private void showCode() {
        Toast.makeText(this, "驗證碼:" + mRandomCode, Toast.LENGTH_SHORT).show();

    }

    public void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

    public void verifyLogin(String phone, String code) {
        showLoading();
        //用handler的延遲操作模擬網路效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                onLoginSuccess();
            } else {
                onLoginFail();
            }
        }, 1000);
    }

    @Override
    public void onLoginSuccess() {
        hideLoading();
        showMessage("登入成功");
    }

    @Override
    public void onLoginFail() {
        hideLoading();
        showMessage("登入失敗");
    }

    @OnClick({R.id.btn_code, R.id.btn_login})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_code:
                sentCode();
                break;
            case R.id.btn_login:
                login();
                break;
            default:
        }
    }
}
複製程式碼

到這裡我們就將一個很簡單幾十行程式碼的Activity,變成了擁有介面的Acitivity,並且程式碼量翻倍到了100行。這麼一看,還算的上優化程式碼嗎? 其實雖然程式碼量增加了,但類中的許多方法變得更加精簡,每個方法負責的任務變少了,這也是程式設計思想中重要的”單一職責原則“的體現:每一個方法只執行它相應的職責,如果有超出它職責範圍的內容,交由其他方法去做就好了。

這樣對程式碼一番優化下來,整體的閱讀性增加了,在需求變動的時候也更方便改動程式碼了。但是到此我們的優化之路只走了一半,還剩下的內容,便是MVP了。

限於文章長度,MVP的內容放到下一篇文章再去詳細闡述,這一篇文章就當作MVP實現前的準備吧。因為文中包含了很多我個人主觀的理解,所有有些內容可能講的不是很正確,歡迎大家指正和給出意見。

如果看官大人們覺得這篇文章還不錯或者幫助到了你,希望能給個小小的點贊和關注,你們的鼓勵就是我最大的動力啦,下篇文章見~

相關文章