用MVP架構開發Android應用
摘要
本文原創,轉載請注明地址: http://kymjs.com/code/2015/11/09/01c今天為大家講述一種在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存在的問題
但是在用的時候會出現一些問題:
- 例如當應用進入后臺且內存不足的時候,系統是會回收這個Activity的。通常我們都知道要用OnSaveInstanceState()去保存狀態,用OnRestoreInstanceState()去恢復狀態。 但是在我們的MVP中,View層是不應該去直接操作Model的,這樣做不合理,同時也增大了M與V的耦合。
- 界面復用問題。通常我們在APP最初版本中是無法預料到以后會有什么變動的,例如我們最初使用一個Fragment去作為界面的顯示,后來在版本變動中發 現這個Fragment越來越龐大,而Fragment的生命周期又太過復雜造成很多難以理解的BUG,我們需要把這個界面放到一個Activity中實 現。這時候就麻煩了,要把Fragment轉成Activity,這可不僅僅是改改類名的問題,更多的是一大堆生命周期需要去修改。例如參考文章2中的譯 者就遇到過這樣的問題。
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 的時候。這的確會有點麻煩,目前的想法是通過集合來解決。