天天看點

【騰訊Bugly幹貨分享】一步一步實作Android的MVP架構

本文來自于騰訊bugly開發者社群,非經作者同意,請勿轉載,原文位址:http://dev.qq.com/topic/5799d7844bef22a823b3ad44

内容大綱:

  1. Android 開發架構的選擇
  2. 如何一步步搭建分層架構
  3. 使用 RxJava 來解決主線程發出網絡請求的問題
  4. 結語

    一、Android開發架構的選擇

    由于原生 Android 開發應該已經是一個基礎的 MVC 架構,是以在初始開發的時候并沒有遇到太多架構上的問題,可是一旦項目規模到了一定的程度,就需要對整個項目的代碼結構做一個總體上的規劃,最終的目的是使代碼可讀,維護性好,友善測試。’
    隻有項目複雜度到了一定程度才需要使用一些更靈活的架構或者結構,簡單來說,寫個 Hello World 并不需要任何第三方的架構
    原生的 MVC 架構遇到大規模的應用,就會變得代碼難讀,不好維護,無法測試的囧境。是以,Android 開發方面也有很多對應的架構來解決這些問題。
    建構架構的最終目的是增強項目代碼的可讀性 ,維護性 和友善測試 ,如果背離了這個初衷,為了使用而使用,最終是得不償失的
    從根本上來講,要解決上述的三個問題,核心思想無非兩種:一個是分層 ,一個是子產品化 。兩個方法最終要實作的就是解耦,分層講的是縱向層面上的解耦,子產品化則是橫向上的解耦。下面我們來詳細讨論一下 Android 開發如何實作不同層面上的解耦。
    解耦的常用方法有兩種:分層 與子產品化

    橫向的子產品化對大家來可能并不陌生,在一個項目建立項目檔案夾的時候就會遇到這個問題,通常的做法是将相同功能的子產品放到同一個目錄下,更複雜的,可以通過插件化來實作功能的分離與加載。

    縱向的分層,不同的項目可能就有不同的分法,并且随着項目的複雜度變大,層次可能越來越多。

    對于經典的 Android MVC 架構來說,如果隻是簡單的應用,業務邏輯寫到 Activity 下面并無太多問題,但一旦業務逐漸變得複雜起來,每個頁面之間有不同的資料互動和業務交流時,activity 的代碼就會急劇膨脹,代碼就會變得可讀性,維護性很差。

    是以這裡我們就要介紹 Android 官方推薦的 MVP 架構,看看 MVP 是如何将 Android 項目層層分解。

    二、如何一步步搭建分層架構

    如果你是個老司機,可以直接參考下面幾篇文章(可在 google 搜到):
  5. Android Application Architecture
  6. Android Architecture Blueprints - Github
  7. Google 官方 MVP 示例之 TODO-MVP - 簡書
  8. 官方示例1-todo-mvp - github
  9. dev-todo-mvp-rxjava - github

    當然如果你覺得看官方的示例太麻煩,那麼本文會通過最簡潔的語言來講解如何通過 MVP 來實作一個合适的業務分層。

    對一個經典的 Android MVC 架構項目來講,它的代碼結構大概是下面這樣(圖檔來自參考文獻)

    簡單來講,就是 Activity 或者 Fragment 直接與資料層互動,activity 通過 apiProvider 進行網絡通路,或者通過 CacheProvider 讀取本地緩存,然後在傳回或者回調裡對 Activity 的界面進行響應重新整理。

    這樣的結構在初期看來沒什麼問題,甚至可以很快的開發出來一個展示功能,但是業務一旦變得複雜了怎麼辦?
    我們作一個設想,假如一次資料通路可能需要同時通路 api 和 cache,或者一次資料請求需要請求兩次 api。對于 activity 來說,它既與界面的展示,事件等有關系,又與業務資料層有着直接的關系,無疑 activity 層會極劇膨脹,變得極難閱讀和維護。
    在這種結構下, activity 同時承擔了 view 層和 controller 層的工作,是以我們需要給 activity 減負

    是以,我們來看看 MVP 是如何做這項工作的(圖檔來自參考文獻)

    這是一個比較典型的 MVP 結構圖,相比于第一張圖,多了兩個層,一個是 Presenter 和 DataManager 層。

    所謂自古圖檔留不住,總是代碼得人心。下面用代碼來說明這個結構的實作。

    首先是 View 層的 Activity,假設有一個最簡單的從 Preference 中擷取字元串的界面

    public class MainActivity extends Activity implements MainView {
    
     MainPresenter presenter;
     TextView mShowTxt;
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         mShowTxt = (TextView)findViewById(R.id.text1);
         loadDatas();
     }
    
     public void loadDatas() {
         presenter = new MainPresenter();
         presenter.addTaskListener(this);
         presenter.getString();
     }
    
     @Override
     public void onShowString(String str) {
         mShowTxt.setText(str);
     }
    }
               

    Activity 裡面包含了幾個檔案,一個是 View 層的對外接口 MainView,一個是P層 Presenter

    首先對外接口 MainView 檔案

    public interface MainView {
     void onShowString(String json);
    }
               
    因為這個界面比較簡單,隻需要在界面上顯示一個字元串,是以隻有一個接口 onShowString,再看P層代碼
    public class MainPresenter {
    
     MainView mainView;
     TaskManager taskData;
    
     public MainPresenter() {
         this.taskData = new TaskManager(new TaskDataSourceImpl());
     }
    
     public MainPresenter test() {
         this.taskData = new TaskManager(new TaskDataSourceTestImpl());
         return this;
     }
    
     public MainPresenter addTaskListener(MainView viewListener) {
         this.mainView = viewListener;
         return this;
     }
    
     public void getString() {
         String str = taskData.getTaskName();
         mainView.onShowString(str);
     }
    
    }
               
    可以看到 Presenter 層是連接配接 Model 層和 View 層的中間層,是以持有 View 層的接口和 Model 層的接口。這裡就可以看到 MVP 架構的威力了,通過接口的形式将 View 層和 Model 層完全隔離開來。
    接口的作用類似給層與層之間制定的一種通信協定,兩個不同的層級互相交流,隻要遵守這些協定即可,并不需要知道具體的實作是怎樣

    看到這裡,有人可能就要問,這跟直接調用有什麼差別,為什麼要大費周章的給 view 層和 Model 層各設定一個接口呢?具體原因,我們看看 Model 層的實作類就知道了。

    下面這個檔案是 DataManager.java,對應的是圖中的 DataManager 子產品

    /**
    * 從資料層擷取的資料,在這裡進行拼裝群組合
    */
    public class TaskManager {
     TaskDataSource dataSource;
    
     public TaskManager(TaskDataSource dataSource) {
         this.dataSource = dataSource;
     }
    
     public String getShowContent() {
         //Todo what you want do on the original data
         return dataSource.getStringFromRemote() + dataSource.getStringFromCache();
     }
    }
               
    TaskDataSource.java 檔案
    /**
    * data 層接口定義
    */
    public interface TaskDataSource {
     String getStringFromRemote();
     String getStringFromCache();
    }
               
    TaskDataSourceImpl.java 檔案
    public class TaskDataSourceImpl implements TaskDataSource {
     @Override
     public String getStringFromRemote() {
         return "Hello ";
     }
    
     @Override
     public String getStringFromCache() {
         return "World";
     }
    }
               
    TaskDataSourceTestImpl.java 檔案
    public class TaskDataSourceTestImpl implements TaskDataSource {
     @Override
     public String getStringFromRemote() {
         return "Hello ";
     }
    
     @Override
     public String getStringFromCache() {
         return " world Test ";
     }
    }
               
    從上面幾個檔案來看, TaskDataSource.java 作為資料層對外的接口, TaskDataSourceImpl.java 是資料層,直接負責資料擷取,無論是從api獲得,還是從本地資料庫讀取資料,本質上都是IO操作。 TaskManager 是作為業務層,對擷取到的資料進行拼裝,然後交給調用層。
    這裡我們來看看分層的作用

    首先來講業務層 TaskManager,業務層的上層是 View 層,下層是 Data 層。在這個類裡,隻有一個 Data 層的接口,是以業務層是不關心資料是如何取得,隻需要通過接口獲得資料之後,對原始的資料進行組合和拼裝。因為完全與其上層和下層分離,是以我們在測試的時候,可以完全獨立的是去測試業務層的邏輯。

    TaskManager 中的 construct 方法的參數是資料層接口,這意味着我們可以給業務層注入不同的資料層實作。

    正式線上釋出的時候注入 TaskDataSourceImpl 這個實作,在測試業務層邏輯的時候,注入 TaskDataSourceTestImpl.java 實作。

    這也正是使用接口來處理每個層級互相通信的好處,可以根據使用場景的不用,使用不同的實作
    到現在為止一個基于 MVP 簡單架構就搭建完成了,但其實還遺留了一個比較大的問題。
    Android 規定,主線程是無法直接進行網絡請求,會抛出 NetworkOnMainThreadException 異常
    我們回到 Presenter 層,看看這裡的調用。因為 presenter 層并不知道業務層以及資料層到底是從網絡擷取資料,還是從本地擷取資料(符合層級間互相透明的原則),因為每次調用都可能存在觸發這個問題。并且我們知道,即使是從本地擷取資料,一次簡單的IO通路也要消耗10MS左右。是以多而複雜的IO可能會直接引發頁面的卡頓。
    理想的情況下,所有的資料請求都應當線上程中完成,主線程隻負責頁面渲染的工作
    當然,Android 本身提供一些方案,比如下面這種:
    public void getString() {
     final Handler mainHandler = new Handler(Looper.getMainLooper());
     new Thread(){
         @Override
         public void run() {
             super.run();
             final String str = taskData.getShowContent();
             mainHandler.post(new Runnable() {
                 @Override
                 public void run() {
                     mainView.onShowString(str);
                 }
             });
         }
     }.start();
    }
               

    通過建立子線程進行IO讀寫擷取資料,然後通過主線程的 Looper 将結果通過傳回主線程進行渲染和展示。

    但每個調用都這樣寫,首先是建立線程會增加額外的成功,其次就是代碼看起來很難讀,縮進太多。

    好在有了 RxJava ,可以比較友善的解決這個問題。

    三、使用RxJava來解決主線程發出網絡請求的問題

    RxJava 是一個天生用來做異步的工具,相比 AsyncTask, Handler 等,它的優點就是簡潔,無比的簡潔。

    在 Android 中使用 RxJava 需要加入下面兩個依賴

    compile 'io.reactivex:rxjava:1.0.14' 
    compile 'io.reactivex:rxandroid:1.0.1'
               
    這裡我們直接介紹如何使用 RxJava 解決這個問題,直接在 presenter 中修改調用方法 getString
    public class MainPresenter {
    
     MainView mainView;
     TaskManager taskData;
    
     public MainPresenter() {
         this.taskData = new TaskManager(new TaskDataSourceImpl());
     }
    
     public MainPresenter test() {
         this.taskData = new TaskManager(new TaskDataSourceTestImpl());
         return this;
     }
    
     public MainPresenter addTaskListener(MainView viewListener) {
         this.mainView = viewListener;
         return this;
     }
    
     public void getString() {
         Func1 dataAction = new Func1<String,String>() {
                 @Override
                 public String call(String param) {
                     return  taskData.getTaskName();
                 }
             }    
         Action1 viewAction = new Action1<String>() {
                 @Override
                 public void call( String str) {
                     mainView.onShowString(str);
                 }
             };        
         Observable.just("")
             .observeOn(Schedulers.io())
             .map(dataAction)
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(view);
    
     }
    
    }
               

    簡單說明一下,與業務資料層的互動被定義到 Action1 裡,然後交由 rxJava,指定 Schedulers.io() 擷取到的線程來執行。Shedulers.io() 是專門用來進行IO通路的線程,并且線程會重複利用,不需要額外的線程管理。而資料傳回到 View 層的操作是在 Action1 中完全,由 rxJava 交由 AndroidSchedulers.mainThread() 指定的UI主線程來執行。

    從代碼量上來講,似比上一種方式要更多了,但實際上,當業務複雜度成倍增加的時候,RxJava 可以采用這種鍊式程式設計方式随意的增加調用和傳回,而實作方式要比前面的方法靈活得多,簡潔得多。

    具體的内容就不在這裡講了,大家可以看參考下面的文章(可在 google 搜到):

  10. 給 Android 開發者的 RxJava 詳解
  11. RxJava 與 Retrofit 結合的最佳實踐
  12. RxJava使用場景小結
  13. How To Use RxJava

    RxJava 的使用場景遠不止這些,在上面第三篇文章提到了以下幾種使用場景:

  14. 取資料先檢查緩存的場景
  15. 需要等到多個接口并發取完資料,再更新
  16. 一個接口的請求依賴另一個API請求傳回的資料
  17. 界面按鈕需要防止連續點選的情況
  18. 響應式的界面
  19. 複雜的資料變換

    四、結語

    至此為止,通過 MVP+RxJava 的組合,我們已經建構出一個比較靈活的 Android 項目架構,總共分成了四部分:View 層,Presenter 層,Model 業務層,Data 資料持久化層。這個架構的優點大概有以下幾點:
  • 每層各自獨立,通過接口通信
  • 實作與接口分離,不同場景(正式,測試)可以挂載不同的實作,友善測試和開發寫假資料
  • 所有的業務邏輯都在非UI線程中進行,最大限度減少IO操作對UI的影響
  • 使用 RxJava 可以将複雜的調用進行鍊式組合,解決多重回調嵌套問題
    當然,這種方式可能還存在着各種各樣的問題,歡迎同學們提出建議

    更多精彩内容歡迎關注bugly的微信公衆賬号:

    騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!