用MVP架構開發Android應用

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

 

摘要

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

怎樣從架構級別去搭建一個APP,怎樣讓他應對日益更改的界面與業務邏輯?今天為大家講述一種在Android上實現MVP模式的方法。

  • 為什么需要MVP
  • 現有的MVP方案

    • AndroidMVP使用示例
    • AndroidMVP存在的問題
    • </ul> </li>

    • 解決現有方案的問題
    • TheMVP原理介紹
    • TheMVP代碼說明
    • 結合DataBinding

      • DataBinding存在的問題
      • 為Presenter添加ViewModel的功能
      • 雙向綁定
      • </ul> </li>

      • 讓MVP變得好用

        • 使用泛型解耦
        • 注解初始化控件
        • 設置監聽
        • 利用變長數組構建View集合
        • </ul> </li>

        • 簡單Demo
        • 參考文章
        • </ul>

          今天為大家講述一種在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)職責分離,減小類體積,使項目結構更加清晰。

          </div>

          現有的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

          </div>

          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();
              //做邏輯操作
            }
          }

          AndroidMVP存在的問題

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

          1. 例如當應用進入后臺且內存不足的時候,系統是會回收這個Activity的。通常我們都知道要用OnSaveInstanceState()去保存狀態,用OnRestoreInstanceState()去恢復狀態。 但是在我們的MVP中,View層是不應該去直接操作Model的,這樣做不合理,同時也增大了M與V的耦合。

          2. 界面復用問題。通常我們在APP最初版本中是無法預料到以后會有什么變動的,例如我們最初使用一個Fragment去作為界面的顯示,后來在版本變動中發現這個Fragment越來越龐大,而Fragment的生命周期又太過復雜造成很多難以理解的BUG,我們需要把這個界面放到一個Activity中實現。這時候就麻煩了,要把Fragment轉成Activity,這可不僅僅是改改類名的問題,更多的是一大堆生命周期需要去修改。例如參考文章2中的譯者就遇到過這樣的問題。

          3. Activity本身就是Android中的一個Context。不論怎么去封裝,都難以避免將業務邏輯代碼寫入到其中。

          </div>

          解決現有方案的問題

          既然知道了這些問題,我們的解決辦法自然是不要將Activity作為View層而去單獨包含Presenter類進來。反過來,我們將 Activity(Fragment)作為Presenter層的代碼,包含一個View層的類來。如果你同時是一名IOS開發者,你一定會很熟悉,這不就是ViewController和APPDelegate嗎。

          使用Activity作為Presenter的優點就在于,可以原封不動的使用Activity本身的生命周期去處理項目邏輯,而不需要強加給另一個包含類,甚至記憶額外自定義的生命周期。

          而同時作為獨立的View層,我們的視圖可以原封不動的傳遞給Presenter(不管是Activity或者Fragment),而不需要改任何代碼。對于一個開發團隊,完全可以將View層的東西交給一個人編寫,而將業務實現交給另一個人編寫。而隨著邏輯變化對View的更改,只需要通過 Presenter層的包含一個代理對象————ViewDelegate來操作相應的更改方法就夠了。

          </div>

          TheMVP原理介紹

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

          </div>

          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允許你使用這樣的代碼為控件設置內容。

          </div>

          <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="http://schemas.android.com/apk/res/android">
            <data>
              <variable name="user" type="com.xxx.User" />
            </data>
            <!--原先的根節點(Root Element)-->
            <LinearLayout>
            ....
            </LinearLayout>
          </layout>

          然后還必須在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);
          }

          雖然簡化了試圖與數據綁定的代碼,但同時也犧牲了布局文件的可復用性。例如我們經常會遇到的,一個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變得好用

            使用泛型解耦

            現在我們是實現了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);
            }

            設置監聽

            同時你也可以一次對多個控件設置監聽事件,例如這樣同時對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);
            

            } }</pre>

            利用變長數組構建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);
            }
            }
            }

            簡單Demo

            • 完整的Demo源碼已經提交在了項目中,你可以在 這里查看 ,運行名為demo的module。

              這里僅取一個簡單的示例。首先是View層的實現

            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);
            }
            }

            接著是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>

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