1. 程式人生 > >淺談Android中的MVP與動態代理

淺談Android中的MVP與動態代理


今日快訊

2月27日,樂視網釋出公告稱,2017年營業收入74.63億元,較去年同期下降66.06%;虧損116億元,由盈轉虧;基本每股收益為-2.9146元。報告期內,由於持續受到關聯方資金緊張、流動性風波影響,社會輿論持續發酵並不斷擴大,對公司聲譽和信用度造成較大影響,公司的廣告收入、終端收入及會員收入等業務均出現較大幅度的下滑。同時,公司日常運營成本以及融資成本卻不斷增加。

作者簡介

本篇來自  Jimmy_Yyj 的投稿,分享了Android中的MVP與動態代理的結合 ,一起來看看!希望大家喜歡。

 Jimmy_Yyj 的部落格地址:

http://blog.csdn.net/yang542397

前言

在Android開發平臺上接觸MVP足足算起來大概已經有一個年頭左右。從最開始到現在經歷的幾個專案中我都採用了MVP架構作為底層框架,使得在view(Activity/Fragment)層中的業務呼叫邏輯分離到另外的presenter層中,讓view層變得非常的輕量,並且不會出現非常複雜的邏輯以及難以閱讀和理解的程式碼塊,並且對於編寫單元測試用例的實現也是非常的方便和快捷的。

MVC簡介

在討論MVP之前我想先討論一下Android傳統開發中一直預設使用的MVC架構,還記得當初做的第一次專案就是基於MVC的。


MVC分為:Model(資料抽象)、View(檢視)、Controller(控制器)的三層架構。接下來我們分別來一一解析每一層所對應的職責分別是什麼。

  • View層:對應的則是Android中的layout資料夾中的xml檔案,在啟動Activity/Fragment的時候,都會載入一個R.layout.xxx的佈局檔案,使得在檢視中顯示出我們在xml中定義好的檢視。

  • Controller層:對應的則是Activity/Fragment。當Activity/Fragment載入了layout檔案後,我們需要在Activity/Fragment中findViewById(int)去尋找到相對應的view,並對找到的view設定相應的屬性以及監聽器。而在設定view的屬性之前,我們很有可能會先到model中請求一次資料,當資料回調回來後controller就會去更新view了。

  • Model層:對應的則是一些DataSource以及DataBean的相關物件,這裡的DataSource指的是資料的來源。一般資料的來源有2個主要的地方,一個是sqlite,一個是webservice,而我們習慣於將這兩種資料的來源封裝在一個repository中,對於呼叫者而言只需要呼叫repository中的一個獲取介面來獲取資料,但是這個資料是從記憶體中還是sqlite還是webservice來,我們都不得而知,從保護了呼叫實現的邏輯,分解相關的實現,達到呼叫者的極度簡單與簡潔,且在單元測試中測試介面也是非常方便的。

我們簡單的瞭解了一下MVC的分層結構後,我們來更加詳細的分析一下在Android中,這三層分別是如何相互呼叫與通訊的。

首先是View:Activity_view.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <Button
       android:id="@+id/btn_hello_mvc"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="Hello MVC" />

</FrameLayout>

接下來是Controller:ControllerActivity.java

// Controller
public class ControllerActivity extends Activity {

   private Button mBtn;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.Activity_view);

       // 在此處,controller呼叫並訪問了view
       mBtn = (Button) findViewById(R.id.btn_hello_mvc);
       mBtn.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               // 對於這個 OnClickListener,是屬於view的,它是view的監聽器
               // 在這裡,view直接訪問了model
               String btnClickData = ModelDataSource.ins().getBtnClickData();
               Toast.makeText(ControllerActivity.this, btnClickData, Toast.LENGTH_SHORT).show();
           }
       });

       // 在此處controller呼叫了model
       String btnText = ModelDataSource.ins().getBtnText();

       // 在此處controller設定了view的屬性
       mBtn.setText(btnText);
   }
}

最後則是Model:ModelDataSource.java

// Model
public class ModelDataSource {

   private static ModelDataSource mInstance = null;

   public static ModelDataSource ins() {
       if (mInstance == null) {
           synchronized (ModelDataSource.class) {
               if (mInstance == null) {
                   mInstance = new ModelDataSource();
               }
           }
       }
       return mInstance;
   }

   private ModelDataSource() {
   }

   public String getBtnText() {
       // 在這裡,
       // 我們可以去資料庫中查詢資料,
       // 也可以去網路中獲取資料
       return "I am from ModelDataSource";
   }

   public String getBtnClickData() {
       // 在這裡,
       // 我們可以去資料庫中查詢資料,
       // 也可以去網路中獲取資料
       return "Hello MVC!";
   }
}

在這裡,我將model設定為了單例模式,我之所以採用單例,是因為model主要關注的是資料來源,而整個模組的資料應該是保證資料的唯一性,這樣無論在任何一處修改資料的時候,都可以在每一處都達到資料的統一性,從而保證了資料的安全。就好像一個賬號對應的是一份密碼一樣。而DataBean則是簡單的String型別了,我並沒有去定義一個數據結構。

從上面的程式碼中,很顯然我們可以很直接的看到View層所表現出的職責是非常的簡單的,就是在xml中編寫好所需的佈局程式碼,向用戶呈現出檢視ui,並且響應使用者的點選以及各種touch互動事件的響應,其中onClickListener中的onCLick()事件則是view層所響應的處理點,在這個click的響應中view直接呼叫了model進行資料的獲取,拿到資料後並及時的響應。對於view的事件響應和生命週期基本上是依賴於controller進行實現。

而在controller中,它的職責邏輯相對的複雜,它對於view需要將從model中獲取而來的資料進行及時的呈現在ui上;而對於model而言controller將會依據app生命週期的變化對model的資料進行及時的重新整理和獲取,比如當我們接受到一個切換桌布的廣播提醒的時候,此時我們需要在controller中通過呼叫model來獲取新的桌布資料,然後更新到某處的快取物件中,再由快取物件釋出出訂閱,因為一個app有可能在多個地方需要監聽桌布的變動,例如專案C的icon和locker元件的預覽介面,在兩個不同的Fragment中需要同時監聽桌布的改變,為了更及時的更新到檢視ui上。而在這個demo中,我只是在onCreate(bundle)的時候從model中獲取了初始的資料然後更新到btn中並沒有做過多通訊,但即便如此也可以很直接的看出controller會因為生命週期的變化對model的資料進行良好的CRUD。

最後一個model層很多人會理解為是普通的javabean以及我的大學老師也是這麼和我說的,但是我並不這麼認為,我不認為model只是很簡單的一個數據結構定義,更多的它應該包含大量的資料處理和運算的邏輯,例如從資料庫中採集資料的操作或者通過網路請求或者通過NetStream的方法來獲取到二進位制的資料,接著將這些二進位制轉換為我們設定好的javabean也就是我們定義好的抽象資料模型,然後該物件進行傳遞以及顯示到檢視ui上。具體的model架構邏輯我希望下次在做更加詳細討論。

Demo執行結果: 

至此,我們大概簡單的介紹完了MVC接下來我們用時序圖的方式來做一個總結:

通過時序圖我們可以大致的看出整個過程中主要的依賴在與controller,controller不止要處理ui的呈現與事件的響應並且還需要負責和model的通訊,且view層也會與model之間通訊,三者之間強強關聯。

最後我們給出它的優缺點:

  • 優點:Android開發中預設使用的框架,易於上手,能在不需要考慮太多需求的情況下快速開發一些小型demo功能app。

  • 缺點:隨著業務的擴充套件controller會變的越來越臃腫和複雜,大大增加了開發人員的維護成本以及交接成本,使得後期工作難以展開,且隨著邏輯的複雜變化以及時間的推移會出現連開發人員自身都對當前程式碼邏輯的複雜造成錯誤的理解。

在這裡由於是demo所以Controller的程式碼並不是很多,但是放入專案中假設這是一個非常複雜的view,例如瀏覽器,它不只要處理頂部的溫度天氣的ui顯示和業務邏輯還要處理底部的資料流以及上下滑動互動搜尋等的一切ui顯示和業務邏輯,如果將這一切的邏輯和呈現都按照MVC來設計,那在整個Activity中的程式碼邏輯將是異常的複雜和混亂並且會使得Activity異常臃腫,使得開發難度急劇上升在專案交接過程中也是需要耗費巨大的成本的,同時維護的成本也是巨大的,當然我這裡只是假設也許事實並非如此哈。

MVP簡介

經過了前面對MVC的討論之後,接下來我們再來討論一下基於MVP的架構實現方案。

從上圖中我們可以很清晰的看到MVP與MVC中的區別:

  • 從Controller變成了Presenter

  • 去除View和Model之間的呼叫關係,從而徹底的分離了Model和View之間的關聯與耦合

MVP和MVC中更具體的區別我們放到後面在做總結與討論,這裡只是大概指出他們兩者之間的不同之處。

還是老規矩,我們分別來介紹一下MVP架構中的:Model(資料模型)、View(檢視)、Presenter(主持者)他們三者的職責以及相互之間的關係到底是如何運作的。

  • View層:檢視層,它所對應的不只是layout中的xml檔案還包括了Activity/Fragment作為檢視的顯示。這樣做是擴大了View層的職責所在,View不僅是設定ui的顯示和屬性並且還包括了生命週期的回撥。

  • Presenter層:主持者層,它相當於是Controller中的業務邏輯部分,它主要是負責view和model層之間的通訊,及時的響應view層的請求並主動的呼叫model層的資料獲取,並且將獲取到的資料結果返回給view層中。presenter是另外新建立一個class,並且讓view從建立的時候就持有一個presenter的例項,當view發生某些請求響應或者生命週期發生變化,則會迅速的向presenter發起請求,讓presenter做出響應的處理,比如:重新整理資料、清除資料防止洩露等。

  • Model層:此處的資料抽象層model和MVC中的model層是一樣的,這裡就不做更多的敘述。

在MVP的架構中,有一個非常大的特點就是view和model之間的通訊必須是通過presenter的傳遞,也正是因為這種隔離的關係,使得檢視和資料之間的關係變得完全分離。當檢視改變的時候,資料來源部分的程式碼無需任何變動;而當資料來源發生改變的時候,檢視部分也根本無需替換。但是事實並非我描述的如此容易,只是在面對整個專案工程的改動來說,我們只需要修改model並且對view層毫無影響,儘管如此工作量依然不容小視。但是如果是使用mvc的預設構建,則會發現整個程式中幾乎處處與model耦合,檢視或互動的替換基本就是對整個專案的重構,成本是相當大的。

在view和presenter兩者之間的通訊並不是想怎麼呼叫就可以怎麼呼叫的,他們之間有著一個標準的協議,就是在兩者之間定義通用介面IContract,在這個interfac中定義了view層中要暴露的介面也定義了presenter層中需要暴露給view的介面,其目的是利用介面的方式將兩者進行隔離,兩者之間誰都不認識誰的實現,達到面向介面程式設計的目的。接下來我們通過程式碼的形式來一起探索MVP在實際程式碼中是如何構建的。

View和Presenter之間的協議IContract.java:

// Contract
public interface IContract {
   interface View {

       void updateBtnText(String s);

       void showToast(String s);
   }

   interface Presenter {

       /**
        * 呼叫該方法表示presenter被激活了
        */

       void start();

       void loadClickString();

       /**
        * 呼叫此方法表示presenter要結束了
        * 其目的是為了接觸相互持有導致的記憶體洩露
        */

       void destroy();
   }
}

View層ViewActivity.java:

// View
public class ViewActivity extends Activity implements IContract.View {

   private Button mBtn;
   private IContract.Presenter mPresenter;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.Activity_view);

       // 在最開始的時候構建presenter
       mPresenter = new Presenter(this);

       // View初始化
       mBtn = (Button) findViewById(R.id.btn_hello_mvp);
       mBtn.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               mPresenter.loadClickString();
           }
       });
   }

   @Override
   protected void onStart() {
       super.onStart();
       mPresenter.start();
   }

   @Override
   protected void onDestroy() {
       if (mPresenter != null) {
           mPresenter.destroy();
           mPresenter = null;
       }
       super.onDestroy();
   }

   @Override
   public void updateBtnText(String s) {
       mBtn.setText(s);
   }

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

Presenter層Presenter.java:

// Presenter
class Presenter implements IContract.Presenter {

   private IContract.View mView;

   Presenter(IContract.View view) {
       mView = view;
   }

   @Override
   public void start() {
       String s = ModelDataSource.ins().getBtnText();
       mView.updateBtnText(s);
   }

   @Override
   public void loadClickString() {
       String s = ModelDataSource.ins().getBtnClickData();
       mView.showToast(s);
   }

   @Override
   public void destroy() {
       mView = null;
   }
}

Model層ModelDataSource.java:

// Model