前言
寫Android:你是如何把Activity寫的如此“萬能”的這篇文章到現在已經好久了,但是由於最近事情較多,寫重構篇的計劃就一直被無情的耽擱下來了,借這幾天還算有點空餘時間,把自己這樁心事瞭解下。
其實大家也知道Android:你是如何把Activity寫的如此“萬能”的這篇文章只是個引子,其實我真正想引出的是mvp設計模式,因為最近自己最近在用mvp做專案,自己對mvp有一些感悟,因此我將用mvp進行“萬能”activity的重構。
同時也有一些朋友與我交流mvp,他們會被mvp中的m,v,p都應該放什麼邏輯而困惑?會被mvp中的m應該怎麼寫而困惑?會被一個介面,怎麼按照mvp來進行重構感到困惑?會被listview的adapter應該放在m層還是p層困惑?
我希望通過本文的講解能幫助大家對mvp有一個更深的瞭解,現在進入主題內容。
正文內容
本文內容分為2部分:
- 帶你瞭解mvp
- 使用mvp對“萬能”Activity進行重構
第一部分會深入瞭解mvp到底是什麼,它的好處等知識。第二部分會講解如何利用mvp來重構“萬能”Activity。
帶你瞭解mvp
任何軟體都是以資料為中心,為了能與使用者進行互動,就需要提供介面支援使用者對資料進行增刪改查操作。
不管是mvc,mvp還是mvvm始終都在做一件事情:怎麼樣能更好的解決資料與介面之間的關係,以達到資料與介面之間的耦合更低,程式碼的複用性更高,程式碼的可測性更好。
本文的重點是講解mvp,因此讓我們開始瞭解下mvp是怎麼組織資料與介面之間的關係的。我們先從mvp的結構圖說起。
mvp的面容
我覺得這張圖是有問題的,問題在於presenter把請求轉交給model,model應該把處理結果返回給presenter,這張圖是沒有反映這個過程的。
正確的mvp的結構圖是這樣子的
我們先看下能從這張圖中得到哪些資訊?
- mvp的分層結構特別類似於網路的七層協議,每層只知道自己依賴層的細節。
- 這種分層的好處是:層與層之間的耦合性低,模組的複用性高,可維護性更好,每層可以單獨存在,這樣可測性更好。
- 資料流的走向可以是:view–>presenter–>model–>presenter–>view,這種資料流一般出現的場景是使用者在介面觸發了一個事件的情形下
- 資料流的走向也可以是:model–>presenter–>view,這種資料流一般出現於比如通過長連結接收訊息的場景。
- 不管資料流是怎樣的一個流動走向,始終有一個原則是:資料流不能跨層流動,即層與層不能跨層通訊。
看了mvp的整體結構圖,我們以從底層到上層的順序依次來介紹model,presenter,view。
model
先說下一些關於model的錯誤理解:
- model是實體類的集合
- 比如從json中解析資料的程式碼應該放在presenter中
- model與mvc中的model是一樣的
關於model的正確理解我們會在文中看到。
資料加工處理廠
通過應用mvp後的感受,我個人的感覺model是最難寫的一層,並且也是最難懂的,因為model是整個應用或介面的資料加工處理廠,所謂資料加工廠就是對資料的獲取,資料的解析,資料的儲存,資料的分發,資料的增刪改查等操作。意思就是凡是涉及到資料操作都是在model進行的,所以model不僅僅只是實體類的集合,同時還包含關於資料的各種處理操作。
三種資料來源
資料的資料來源有三種:記憶體,磁碟(檔案或資料庫等),網路。為了提升app的效能,有必要把經常訪問的資料臨時存入記憶體中;同時也為了提升app效能和為使用者省流量省電,有必要把資料存入磁碟中;還有的資料是有必要從網路讀取的。三個資料來源不一定同時存在,比如不與網路互動的app,不存在網路資料來源。所以凡是涉及到關於資料發生於三個資料來源加工處理的操作的程式碼都要放在model中。
model為上層提供的服務
model從黑盒的角度來看為上層(指依賴於model的層比如present)提供的服務無非就2種:model為上層提供資料,model處理上層傳遞的資料
model為上層提供資料
上層會從model中去資料,那model會從三資料來源中取資料,取的順序是
- 先記憶體,記憶體取到資料返回
- 其次磁碟,磁碟取到資料,如有必要把資料儲存在記憶體中,則需要進行
儲存,返回資料 - 最後網路,網路取到資料,如有必要在磁碟或記憶體中儲存,則進行儲存,返回資料
上面的取資料過程是最簡單的情況,複雜些還會涉及到從記憶體或磁碟中取到的資料是否過期,過期的話就應該從網路獲取。從網路取得資料後需要把記憶體或磁碟的資料更新。
model處理上層傳遞的資料
model接收到上層傳遞的資料後,model會依次把資料扔給三個資料來源去處理,有可能三個資料來源都會處理資料,有可能只是其中一個處理,model會把處理的結果返回。
所以model會把解析好的資料提供給上層,上層對於資料的來源完全是透明的,上層完全不需要關心資料到底是來自記憶體,還是磁碟甚至是網路。同理上層只需要的把資料扔給model,上層唯一做的事情就是愉快的等待處理結果。
tip
mvc中的model是要和view進行互動的,而mvp中的model不會知道任何view的細節。
model中的所有操作都發生於普通執行緒。
關於model的介紹先到此,我們在來看下presenter。
presenter
presenter翻譯成漢語的意思是主持人,提出者。從它的意思可以看出它有控制全場的作用。首先presenter是處於mvp的中間層,在view和model中起一個承上啟下的作用。presenter會把view交給自己的命令進行一定的校驗等操作交給model處理,會把model處理的結果交給view。
presenter封裝業務
presenter不僅起一個橋樑的作用,它還會把業務邏輯程式碼給包攬下來。這樣就可以減輕Activity的負擔了,讓Activity全心全意做它的view工作。那估計就有朋友犯迷糊了,哪些程式碼屬於業務邏輯呢?比如一些校驗程式碼。或者可以這樣想只要是不屬於view和model的程式碼基本都可以放在presenter中。
presenter負責重新整理view
mvc或以前的關於view的寫法一般都是這樣,view在接收到資料後,自己來進行view的重新整理或其他操作。但是mvp中presenter負責對view進行重新整理,比如從model獲取的資料,presenter會根據獲取的資料成功與否來通知view應該是顯示成功介面還是失敗介面。這樣就讓Activity變的更輕了,變成了聽別人指揮的傻白甜了。這時候的presenter就有點主持人,掌控者的味道了。
presenter持有的執行緒
Android中view的操作需要在ui執行緒裡執行,其他耗時操作需要在普通執行緒執行。presenter會持有這2種執行緒:ui執行緒,普通執行緒。重新整理view時,它切換為ui執行緒進行重新整理,從model取資料切換為普通執行緒。假如使用rxjava的話,就特別簡單了關於執行緒切換的事情。
tip
presenter從model中獲取的資料就是解析好的資料,不需要出現解析資料的程式碼。
接著我們來看下view。
view
view層就很好理解了,就是使用者直接看到的介面,mvp中的view是很省心的,比如更新view,接收資料。這些操作它都不需要操心,也不需要知道資料到底來自哪裡,給我啥我顯示啥就可以了。
一個view可以同時擁有多個presenter,也可以只有一個presenter。
Android中的Activity,Fragment在mvp中是作為view來使用的,這些Activity,Fragment的責任就小了,只關心介面相關的事情足矣。
各種Adapter是放在view層的。
總結
我們初步認識了mvp,mvp中的model,present,view到底是什麼,他們之間的關係是什麼樣的,這只是初步認識mvp,關於mvp中還有很多細節需要介紹,比如android clean architecture 中model和presenter之間多了一層interactor,多的這層interactor是用來做什麼的,model層是怎麼架構的。google mvpmodel層要比android clean architecture 簡單等,希望能在我後面的章節看到相關關於每層的詳細介紹。我們開始進入我們的重構”萬能”Activity的部分。
使用mvp設計模式對”萬能”Activity進行重構
回憶下“萬能”Activity的樣子
我在上篇文章的“萬能”的LoginActivity基礎上增加了登入對話方塊的功能,“萬能”LoginActivity的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public LoginActivity extends Activity{ private EditText mUserNameView, mPasswordView; private Button mLoginView; public void initViews(){ ....... 各種findViewById.....程式碼 //給登陸按鈕加監聽器 mLoginView.OnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String userName = mUserNameView.getText(); String password = mPasswordView.getText(); //驗證使用者輸入的密碼是否合法 if(!validate(userName) || !validate(password)){ 告訴使用者輸入的使用者名稱或密碼不合法 } else{ //開始登陸 login(userName,password); } } }); } //登陸方法,用虛擬碼來寫下網路請求 private void login(String userName,String password){ //增加登入進度對話方塊給使用者友好使用者體驗 顯示登入進度對話方塊... HttpClient.getInstance().login(userName,password, new ResponseListener(){ public void failed(Failed failed){ 把登入進度對話方塊消失... 做失敗相關的處理工作,比如給使用者提示 把密碼輸入框清空,還比如登陸次數限制等 } public void success(Response response){ 把登入進度對話方塊消失... 做成功相關的處理工作 //暫且把使用者資訊的類叫做UserInfo,從json中解析資料,假設response.getContent()存在 String jsonContent = response.getContent(); JsonObject jsonObject = new JsonObject(jsonContent); UserInfo userInfo = new UserInfo(); userInfo.name = jsonObject.optString("name"); userInfo.userId = jsonObject.optString("userId"); 其他欄位的解析...... //儲存userInfo資訊到資料表中,假設userDatabase已經存在 userDatabase.save(userInfo); 跳到app的主頁 } }); } //驗證給定的字串是否合法,true 合法,false 不合法 private boolean validate(String str){ } } |
我們回憶了“萬能”LoginActivity的程式碼後,開始重構。
開始重構
model
在使用mvp時,我一般有個習慣就是首先從model->presenter->view的順序寫程式碼,所以重構“萬能”LoginActivity也先從model開始。前半部分關於model介紹過,model從黑盒的角度來說只有2個功能:一個是輸出資料,一個是輸入資料。因此登入中presenter只需要把賬號,密碼交給model,presenter唯一做的事情就是監聽登入狀態即可。model會把presenter傳遞的賬號,密碼交給伺服器,model在把伺服器返回的資料進行解析,儲存在磁碟或記憶體中,把解析好的資料傳遞給presenter。那我們看下虛擬碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//管理登入的類,它是單例的,這就不寫單例方法了 public class LoginManager{ //登入的監聽器 public static interface LoginListener{ //登入成功 void onSuccessLogin(UserEntity user); //登入失敗 void onFailedLogin(Failed failed); } //登入方法 public void login(String name,String password,final LoginListener loginListener){ //假設HttpClient是一個請求網路服務的類 HttpClient.getInstance().login(userName,password, new ResponseListener(){ public void failed(Failed failed){ loginListener.onFailedLogin(failed); } public void success(Response response){ //假設UserParse類已經存在,主要用來從response中解析UserEntity UserEntity userEntity = UserParse(response.getContent()); //假設userDatabase是資料庫儲存類 userDatabase.store(userEntity); //還可以把userEntity存入記憶體中,這得根據業務需求進行處理 loginListener.onSuccessLogin(userEntity); } }); } } |
登入的model層我們沒有做的那麼複雜,比如把伺服器返回的使用者資訊儲存在記憶體中,把伺服器返回的token儲存在磁碟中,實現自動登入功能等,本例子只是一個特別簡單的登入功能,實際應用中登入需要考慮很多的東西,登入的modle層到此重構完畢。
presenter
上文中提到過presenter,presenter起連線view與model的作用,presenter封裝業務作用,presenter有負責重新整理view的作用。
我們梳理下presenter都應該包含哪些功能:
- 驗證賬號,密碼的合法性.
- 把驗證成功的賬號,密碼交給model層
- 把登入的狀態傳遞給view層,並根據不同的登入狀態通知view顯示不同的介面
那讓我們開始寫程式碼,presenter層的類組織結構我是參照google mvp的presenter類組織結構來進行的,因為我認為google mvp presenter類結構更清晰,看下虛擬碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
//登入的條約介面,分別定義了登入view的一些方法,和登入presenter的一些方法 public interface LoginContract{ //需要view層來實現的登入view介面,IView是所有view的基類 interface ILoginView extends IView{ void onShowSuccessLoginView(UserInfo userInfo); void onShowFailedLoginView(int failed); void showLoginingView(); void dissLoginingView(); } //定義了登入presenter的一些方法,IPresenter是所有Presenter的基類 interface ILoginPresenter extends IPresenter<ILoginView>{ void login(String name,String password); } } public interface IView{ void initView(); } //IPresenter提供了一些基礎方法,其實這些方法是對應Activity或Fragment的生命週期方法 public interface IPresenter<V extends IVew>{ void onStop(); void onResume(); void onDestroy(); void onPause(); void onStart(); void init(V view); } //登入的presenter public class LoginPresenter implements ILoginPresenter{ private ILoginView mLoginView; private LoginManager mLoginManager = LoginManager.getInstance(); public void init(ILoginView loginView){ mLoginView = loginView; mLoginView.initView(); } public void login(String name,String password){ //驗證name,password的合法性, if(validate(name) && validate(password)){ //假設NormalThread.exe方法可以讓操作在普通執行緒裡執行 mLoginView.showLoginingView(); NormalThread.exe(new Runnable(){ public void run(){ mLoginManager.login(name,password, new LoginListener(){ public void onSuccessLogin(UserEntity userEntity){ //UserMapper類,負責把底層的UserEntity轉化為view層使用的UserInfo UserInfo userInfo = UserMapper.map(userEntity); //下面的程式碼在ui執行緒中執行,這就不寫具體的實現了 mLoginView.onShowSuccessLoginView(userInfo); mLoginView.dissLoginingView(); } public void onFailedLogin(Failed failed){ //下面的程式碼在ui執行緒中執行,這就不寫具體的實現了 mLoginView.onShowFailedLoginView(failed.failedState); mLoginView.dissLoginingView(); } }); } } }else{ //假設1代表賬號,密碼不合法 mLoginView.onShowFailedLoginView(1); } } } |
以上登入的Presenter層的虛擬碼都是關鍵程式碼,讓我們看下以上程式碼都做了什麼?
- LoginContract 把ILoginView和ILoginPresenter組合在一塊,ILoginView定義了提供給ILoginPresenter的方法,ILoginPresenter定義了提供給ILoginView的方法。只需要看LoginContract就可以知道登入的view層和presenter層之間的約定。我很喜歡XXContract類,也推薦大家使用
- IView是一個基礎介面,提供一些公用方法
- IPresenter是一個基礎介面,它把Activity或Fragment的生命週期方法集合了起來。有了這些生命週期方法,presenter就讓Activity一心一意做它的view相關的工作。
到此登入Present的重構結束,我們重構view層。
view
view層就很簡單了,只是需要把基礎設施建立好,直接看虛擬碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public abstract class BaseActivity extends FragmentActivity{ private Set<IPresenter> mAllPresenters = new HashSet<IPresenter>(1); /** * 獲取layout的id,具體由子類實現 * @return */ protected abstract int getLayoutResId(); /** *需要子類來實現,獲取子類的IPresenter,一個activity有可能有多個IPresenter */ protected abstract IPresenter[] getPresenters(); //初始化presenters, protected abstract void onInitPresenters(); /** * 從intent中解析資料,具體子類來實現 * @param argIntent */ protected void parseArgumentsFromIntent(Intent argIntent){ } private void addPresenters(){ IPresenter[] presenters = getPresenters(); if(presenters != null){ for(int i = 0; i < presenters.length; i++){ mAllPresenters.add(presneters[i]); } } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getLayoutResId()); if(getIntent() != null){ parseArgumentsFromIntent(getIntent()); } addPresenters(); onInitPresents(); } @Override protected void onResume() { super.onResume(); //依次呼叫IPresenter的onResume方法 for (IPresenter presenter:mAllPresenters ) { if(presenter != null){ presenter.onResume(); } } } ...其他生命週期方法也是類似,呼叫IPresenter中相應的生命週期方法... } |
基礎設施已經ok了,這時候我們就該重構”萬能“LoginActivity了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public class LoginActivity extends BaseActivity implements LoginConstract.ILoginView{ private LoginPresenter mLoginPresenter = new LoginPresenter(); protected int getLayoutResId(){ return R.layout.activity_login; } protected IPresenter[] getPresenters(){ return new IPresneter[]{ mLoginPresenter}; } //初始化presenters, protected void onInitPresenters(){ mLoginPresenter.init(this); } public void initView(){ ...初始化view的程式碼... // mLoginButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mLoginPresenter.login(name,password); } }); } public void onShowSuccessLoginView(UserInfo userInfo){ ....顯示登入成功介面.... } public void onShowFailedLoginView(int failed){ ...顯示登入失敗介面... } public void showLoginingView(){ ...顯示登入進度條對話方塊... } public void dissLoginingView(){ ...消失登入進度條對話方塊... } } |
我們著重說下基礎設施BaseActivity:
因為一個Activity是有可能包含多個Presenter的,所以需要在BaseActivity中是有必要把這些Presenter收集起來。
需要在BaseActivity的生命週期方法裡面呼叫每個Presenter的相應週期方法。
重構以後的LoginActivity是不是很清爽,只保留與view相關的邏輯。
小結重構後的類結構
重構“萬能”LoginActivity到此結束,我們小結下重構後的類結構:
- model層包含:LoginManager(登入管理類),UserEntity(使用者實體類,主要使用在model層),UserDatabase(使用者資料表)。
- presenter層包含:LoginConstract(登入約定類,組合ILoginView和ILoginPresenter),LoginPresenter(登入presenter類),UserMapper(負責把model層的UserEntity轉化為view層使用的
UserInfo) - view層包含:BaseActivity(基礎類),LoginActivity(登入Activity),UserInfo(使用者資訊類,主要使用在view層)
重構感悟
疑惑
估計會有細心的朋友發現model層是LoginManager而不是像android clean architecture 中model層使用了respository,我個人覺得model層也沒必要這麼嚴格的按respository的架構方式來組織類結構,因為本例中登入功能實在是太簡單了,所以就用最簡單的一個LoginManager類來供上層呼叫。
優缺點
使用mvp重構“萬能”Activity以後,帶來了以下好處:
- 類的組織結構更清晰
- 每層可以進行單獨的測試
- 類的可複用性更高
- 層與層的耦合性降低
- 可維護性更高
- 每個類儘量做到單一職責
提高生產效率
但同時也帶來了一些缺點,比如建立的類多了很多,管理這些類的成本會增加。但是萬事萬物都有兩面性,就看利與弊的大小了。我個人覺得mvp的利肯定是大於弊的,所以有必要採用這種架構來設計你的app。
以上虛擬碼中我沒有使用rxjava,dagger2,假如把它們應用於mvp中會讓你事半功倍,rxjava可以讓你在寫presenter層和model層時,可以讓presenter與model互動更簡單,可以是model層變的尤為的簡單比如從三大資料來源取資料操作,我們自己用程式碼實現是可以的但是畢竟要花很多時間,但是用rxjava的一些操作符很容易做到。dagger2可以更好的幫助你進行依賴注入, 可以參考下Android:dagger2讓你愛不釋手-基礎依賴注入框架篇 的介紹,還有鼎鼎有名的retrofit也是可以提高效率的。
總結
本文我們瞭解了mvp以及每一層,以及使用mvp來重構“萬能”Activity,其實每一層需要注意的東西還有很多,比如model層是最難寫的一層。希望我能在以後用幾篇文章來介紹每一層更多的細節。還有我現在正在用mvp做的一個專案,等做完了我會上傳到github。歡迎大家指正。
本人微信:704451290
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式