【騰訊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 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM