用MVP架構開發Android應用

jopen 9年前發布 | 19K 次閱讀 Android Android開發 移動開發

摘要

本文原創,轉載請注明地址: http://kymjs.com/code/2015/11/09/01

c今天為大家講述一種在Android上實現MVP模式的方法。也是我從新項目中總結出來的一種新的架構模式,大家可以查看我的TheMVP項目:https://github.com/kymjs/TheMVP

為什么需要MVP

關于什么是MVP,以及MVC、MVP、MVVM有什么區別,這類問題網上已經有很多的講解,你可以自行搜索或看看文末的參考文章,這里就只講講為什么需要MVP。
在Android開發中,Activity并不是一個標準的MVC模式中的Controller,它的首要職責是加載應用的布局和初始化用戶界面,并接受 并處理來自用戶的操作請求,進而作出響應。但是,隨著界面及其邏輯的復雜度不斷提升,Activity類的職責不斷增加,以致很容易變得龐大而臃腫。
越小的類,bug越不容易出現,越容易調試,更容易測試,我相信這一點大家是都贊同的。在MVP模式下,View和Model是完全分離沒有任何直接關聯的(比如你在View層中完全不需要導Model的包,也不應該去關聯它們)。
使用MVP模式能夠更方便的幫助Activity(或Fragment)職責分離,減小類體積,使項目結構更加清晰。

現有的MVP方案

GitHub上有一個開源項目AndroidMVP,其思想是通過將Activity或Fragment看做View,并單獨采用has…a…關系包含一個Presenter類的方式實現的,這是一種可行的技術方案。

AndroidMVP使用示例

完整Demo請查看這里
首先需要定義一個View層接口,讓View實現類Activity(Fragment)實現;
其次需要定義一個Presenter實現接口,讓Presenter實現類實現;
在View實現類Activity(Fragment)中包含Presenter對象,并在Presenter創建的時候傳一個View對象;
在Presenter中通過構造時傳入的視圖層對象操作View

public interface LoginView {
    public void showProgress();
    public void hideProgress();
    public void setUsernameError();
    public void setPasswordError();
}

public class A extends Activity implements LoginView, OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { // ... // 省略初始化控件 // ... presenter = new LoginPresenterImpl(this); } //...省略眾多接口方法 }

public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener { private LoginView loginView;

public LoginPresenterImpl(LoginView loginView) {
    this.loginView = loginView;
}

@Override public void validateCredentials(String username, String password) {
    loginView.showProgress();
    //做邏輯操作
}

}</pre>

AndroidMVP存在的問題

但是在用的時候會出現一些問題:

  1. 例如當應用進入后臺且內存不足的時候,系統是會回收這個Activity的。通常我們都知道要用OnSaveInstanceState()去保存狀態,用OnRestoreInstanceState()去恢復狀態。 但是在我們的MVP中,View層是不應該去直接操作Model的,這樣做不合理,同時也增大了M與V的耦合。
  2. 界面復用問題。通常我們在APP最初版本中是無法預料到以后會有什么變動的,例如我們最初使用一個Fragment去作為界面的顯示,后來在版本變動中發 現這個Fragment越來越龐大,而Fragment的生命周期又太過復雜造成很多難以理解的BUG,我們需要把這個界面放到一個Activity中實 現。這時候就麻煩了,要把Fragment轉成Activity,這可不僅僅是改改類名的問題,更多的是一大堆生命周期需要去修改。例如參考文章2中的譯 者就遇到過這樣的問題。
  3. Activity本身就是Android中的一個Context。不論怎么去封裝,都難以避免將業務邏輯代碼寫入到其中。 </p>

    解決現有方案的問題

    既然知道了這些問題,我們的解決辦法自然是不要將Activity作為View層而去單獨包含Presenter類進來。反過來,我們將 Activity(Fragment)作為Presenter層的代碼,包含一個View層的類來。如果你同時是一名IOS開發者,你一定會很熟悉,這不 就是ViewController和APPDelegate嗎。
    使用Activity作為Presenter的優點就在于,可以原封不動的使用Activity本身的生命周期去處理項目邏輯,而不需要強加給另一個包含類,甚至記憶額外自定義的生命周期。
    而同時作為獨立的View層,我們的視圖可以原封不動的傳遞給Presenter(不管是Activity或者Fragment),而不需要改任何代碼。 對于一個開發團隊,完全可以將View層的東西交給一個人編寫,而將業務實現交給另一個人編寫。而隨著邏輯變化對View的更改,只需要通過 Presenter層的包含一個代理對象————ViewDelegate來操作相應的更改方法就夠了。

    TheMVP原理介紹

    與傳統androidMVP不同(原因上文已經說了),TheMVP使用Activity作為Presenter層來處理代碼邏輯,通過讓Activity包含一個ViewDelegate對象來間接操作View層對外提供的方法,從而做到完全解耦視圖層。如下圖:

    TheMVP代碼說明

    要將Activity作為Presenter來寫,需要讓View變得可復用,必須解決的一個問題就是setContentView()如何調用,因為它是Activity(Fragment有類似)的方法。
    我們需要把視圖抽離出來獨立實現。可以定義一個接口,來限定View層必須實現的方法(這個接口定義,也就是View層的代理對象),例如:

    public interface IDelegate {
     void create(LayoutInflater i, ViewGroup v, Bundle b);
     View getRootView();
    }

    首先通過inflater一個布局,將這個布局轉換成View,再用getRootView()方法把這個View返回給Presenter層,讓setContentView(view)去調用,這樣就實現了rootView的獨立。
    所以,在Presenter層,我們的實現應該是:
    protected void onCreate(Bundle savedInstanceState) {
     //獲取到視圖層對象
     IDelegate viewDelegate = xxx;
     //讓視圖層初始化(如果是Fragment,就需要傳遞onCreateView方法中的三個參數)
     viewDelegate.create(getLayoutInflater(), null, savedInstanceState);
     //拿到初始化以后的rootview,并設置content
     setContentView(viewDelegate.getRootView());
    }

    結合DataBinding

    一個好的架構一定是對擴展開放,對修改關閉的,這是軟件設計模式的開閉原則。
    如果你之前有了解過Google的DataBinding,你一定知道ViewModel的概念。DataBinding 解決了 Android UI 編程中的一個痛點,就是要給一個控件設置內容,必須首先獲取到控件的對象,并調用set方法(例如setText()),傳一個數據進去。
    DataBinding允許你使用這樣的代碼為控件設置內容。

    <TextView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@{user.lastName}" />

    其中user.lastName表示在項目中的數據類的user對象的lastName屬性。

    DataBinding存在的問題

    但是使用 Data Binding 之后,xml的布局文件就不再單純地展示 UI 元素,還需要定義 UI 元素用到的變量。所以,它的根節點不再是一個ViewGroup,而是變成了layout,并且新增了一個節點data。

    <layout xmlns:android=";
     <data>

     <variable name="user" type="com.xxx.User" />
    

    </data> <!--原先的根節點(Root Element)--> <LinearLayout> .... </LinearLayout> </layout></pre>然后還必須在onCreate()方法中,用DatabindingUtil.setContentView()來替換掉setContentView(),然后創建一個 user 對象,通過binding.setUser(user)與 variable 進行綁定。

    protected void onCreate(Bundle savedInstanceState) {
     ActivityBasicBinding binding = DataBindingUtil.setContentView(

         this, R.layout.activity_basic);
    

    User user = new User(); binding.setUser(user); }</pre>

    雖然簡化了試圖與數據綁定的代碼,但同時也犧牲了布局文件的可復用性。例如我們經常會遇到的,一個Fragment與另一個Fragment,在布 局上完全一樣,而僅僅是數據不同,這時我們通常會用代碼去控制在不同的界面顯示不同的數據。然而,如果將數據寫死在xml中,就失去了布局的復用性。
    因此,我們可以嘗試將視圖與數據模型綁定的邏輯抽出,單獨建立一個類來使用代碼控制,這樣如果發生相同的界面復用,只需要重寫視圖與數據綁定的邏輯就夠了,其他的代碼仍然不變。

    為Presenter添加ViewModel的功能

    定義一個ViewModel層的接口,其中包含兩個泛型分別為View層的代理和Model層的代理:

    public interface DataBinder<T extends IDelegate, D extends IModel> {
     /**

    • 將數據與View綁定,這樣當數據改變的時候,框架就知道這個數據是和哪個View綁定在一起的,就可以自動改變ui
    • 當數據改變的時候,會回調本方法。 *
    • @param viewDelegate 視圖層代理
    • @param data 數據模型對象 */ void viewBindModel(T viewDelegate, D data); }</pre>
      為我們之前寫好的Presenter添加擴展,使它能夠支持DataBinder。其中使用getDataBinder()方法,得到開發中具體的某個界 面的ViewModel層的擴展,然后我們只需要在數據改變的時候,手動調用notifyModelChanged()方法,即可使ViewModel中 定義的綁定邏輯生效:

      public abstract class DataBindActivity<T extends IDelegate> extends
       ActivityPresenter<T> {
      protected DataBinder binder;

      public abstract DataBinder getDataBinder();

      public <D extends IModel> void notifyModelChanged(D data) { binder.viewBindModel(viewDelegate, data); } }</pre>

      雙向綁定

      同時使用以代碼控制的邏輯,能夠輕松實現視圖改變數據,數據改變視圖的雙向綁定。這是標準的ViewModel所一定會具備而現在的Beta版DataBinding沒辦法做到的功能。
      剛剛的DataBinder接口已經實現的是 Model->View 的單向綁定,那么我們只需要為其添加一個 View-> Model 的方法,來讓具體界面的ViewModel層實現類來解決其中的邏輯即可。

      public interface DataBinder<T extends IDelegate, D extends IModel> {
      void viewBindModel(T viewDelegate, D data);

      /**

    • 將數據與View綁定,這樣當view內容改變的時候,框架就知道這個View是和哪個數據綁定在一起的,就可以自動改變數據
    • 當ui改變的時候,會回調本方法。 *
    • @param viewDelegate 視圖層代理
    • @param data 數據模型對象 */ void modelBindView(T viewDelegate, D data); }</pre>

      也許你會有疑問,既然是MVP模式的項目,還又加ViewModel層,豈不是不倫不類?這里需要說明的是我們的架構目前仍舊是MVP模式的,你可以看做是在Presenter中,我們額外添加了一個方法,然后我們只在這個方法中寫setText()、setImageResource()這類對控件設值的方法。
      此時,我們的項目結構如下圖:

      讓MVP變得好用

      使用泛型解耦

      現在我們是實現了View與Presenter的解耦,在onCreate中包含了一個接口對象來實現我們固定的一些必須方法。但是又引入了問題: 一些特定方法沒辦法引用了。比如某個界面的設值、控件的修改顯示邏輯對Presenter層的接口,接口對象必須強轉成具體子類才能調用。
      解決辦法:可以通過泛型來解決直接引用具體對象的問題。比如我們可以在子類定義以后確定一個Presenter中所引用的Delegate的具體類型。例如:

      public abstract class ActivityPresenter<T extends IDelegate> extends Activity {
      protected T viewDelegate;

      protected void onCreate(Bundle savedInstanceState) { viewDelegate = getDelegateClass().newInstance(); }

      protected abstract Class<T> getDelegateClass(); }</pre>

      這樣我們在ActivityPresenter的繼承類中就可以通過動態設置getDelegateClass()的返回值來確定Delegate的具體類型了。

      注解初始化控件

      遺憾的是沒辦法使用編譯時注解綁定控件,例如Butterknife;不過你依然可以使用運行時注解,例如afinal。不過也不推薦使用運行時注解,畢竟通過反射去初始化控件會很費時間。
      當然,解決辦法也是有的:就是通過定義findViewById()泛型類類型返回值,這樣我們就不用寫那又臭又長的函數名加強轉了。

      public <T extends View> T bindView(int id) {
      T view = (T) mViews.get(id);
      if (view == null) {
       view = (T) rootView.findViewById(id);
       mViews.put(id, view);
      }
      return view;
      }

public <T extends View> T get(int id) { return (T) bindView(id); }</pre>

設置監聽

同時你也可以一次對多個控件設置監聽事件,例如這樣同時對button1,button2,button3設置監聽器listener

viewDelegate.setOnClickListener(listener, R.id.button1, R.id.button2, R.id.button3);

它的內部實現也很簡單,就是利用了變參函數

public void setOnClickListener(OnClickListener l, int... ids) {
        if (ids == null) {
            return;
        }
        for (int id : ids) {
            get(id).setOnClickListener(l);
        }
    }

利用變長數組構建View集合

由于Presenter在使用訪問View的時候并不是直接調用,而是通過代理對象間接調用,如果我們在實現View層代碼的時候有太多的控件需要被引用,可能就必須定義一大堆控件聲明,會造成記憶負擔。
這時候顯然通過id去記憶更方便一些。我們可以使用SparseArray它是由兩個數組來替代Map操作的類(如果你還是不知道他是干嘛的,可以簡單的當成HashMap)。

結合上面的全部例子,可以為IDelegate接口定義一個抽象類,將全部的工具方法都集成進來

public abstract class AppDelegate implements IDelegate {
    protected final SparseArray<View> mViews = new SparseArray<View>();

protected View rootView;

public abstract int getRootLayoutId();

@Override
public void create(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    int rootLayoutId = getRootLayoutId();
    rootView = inflater.inflate(rootLayoutId, container, false);
}

@Override
public View getRootView() {
    return rootView;
}

public <T extends View> T bindView(int id) {
    T view = (T) mViews.get(id);
    if (view == null) {
        view = (T) rootView.findViewById(id);
        mViews.put(id, view);
    }
    return view;
}

public <T extends View> T get(int id) {
    return (T) bindView(id);
}

public void setOnClickListener(View.OnClickListener listener, int... ids) {
    if (ids == null) {
        return;
    }
    for (int id : ids) {
        get(id).setOnClickListener(listener);
    }
}

}</pre>

簡單Demo

  • 完整的Demo源碼已經提交在了項目中,你可以在這里查看,運行名為demo的module。
    這里僅取一個簡單的示例。首先是View層的實現
  • </ul>

    public class SimpleDelegate extends AppDelegate {
        @Override
        public int getRootLayoutId() {
            return R.layout.delegate_simple;
        }

    @Override
    public void initWidget() {
        super.initWidget();
        TextView textView = get(R.id.text);
        textView.setText("在視圖代理層創建布局");
    }
    
    public void setText(String text) {
        //get(id)等同于bindview(id),從上文就可以看到了,get方法調用了bindview方法
        TextView textView = get(R.id.text);
        textView.setText(text);
    }
    

    }</pre>

    接著是Presenter層的實現

    /**

    • 在這里做業務邏輯操作,通過viewDelegate操作View層控件 */ public class SimpleActivity extends ActivityPresenter<SimpleDelegate> implements OnClickListener {

      @Override protected Class<SimpleDelegate> getDelegateClass() {

       return SimpleDelegate.class;
      

      }

      /**

      • 在這里寫綁定事件監聽相關方法 */ @Override protected void bindEvenListener() { super.bindEvenListener(); viewDelegate.get(R.id.button1).setOnClickListener(this); }

        @Override public void onClick(View v) { switch (v.getId()) {

         case R.id.button1:
             viewDelegate.setText("你點擊了button");
             break;
        

        } } }</pre>

        參考文章

        《MVC, MVP, MVVM比較以及區別》作者Justrun
        《android實現MVP的新思路》譯者FTExplore

        補充

        11月10日補充
        感謝Rocko指出的問題,就是一個 View 需要幾個 Model 的時候。這的確會有點麻煩,目前的想法是通過集合來解決。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!