Android MVP模式從入門到進門(一)
今天是2018年最後一個工作日了,提前祝大家新年快樂啦~~~
這是一篇面向Android初學者拋磚引玉的文章,正如以前的我——寫程式碼只考慮如何實現功能,對於設計模式完全沒有想法和認知。在這篇文章中,我會通過一個常用的登入場景,從幾十行程式碼的直接實現,一步步構建出入門級的MVP架構,向你們分享我所理解的程式碼的流暢性。但限於文章長度,本篇先對實現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 的事件處理。 其中主要包含兩個方法:
-
void showCode()
點選獲取驗證碼按鈕時呼叫,因為是測試環境,所以直接生成6位隨機數作為驗證碼並顯示出來,同時傳入全域性變數
mRandomCode
中以作登入校驗用。 -
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實現前的準備吧。因為文中包含了很多我個人主觀的理解,所有有些內容可能講的不是很正確,歡迎大家指正和給出意見。
如果看官大人們覺得這篇文章還不錯或者幫助到了你,希望能給個小小的點贊和關注,你們的鼓勵就是我最大的動力啦,下篇文章見~