用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>
- 將數據與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>
今天為大家講述一種在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> { /**
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關資訊
sesese色