MVP是從MVC衍生出來的,因此,先說一下MVC,再到MVP。java
MVC分爲三個部分:android
視圖(View):用戶界面。git
控制器(Controller):業務邏輯,用於控制應用程序的流程。github
模型(Model):數據處理。即網絡請求、數據庫等一些對數據的操做處理。數據庫
它們之間的交互一般以下圖:編程
那麼在咱們android裏,如何劃分這幾層?在這裏,參考部分博文,以及根據我我的的理解,列出下面兩種可能的狀況:bash
(一)View:xml。Controller:Activity/Fragment。Model:數據處理。網絡
若是是這樣劃分的話。實際運用中,每每會出現Activity/Fragment雖然劃分爲Controller層,但看起來又像View層。架構
爲何?由於若是隻用xml做爲View層,對界面的控制能力實在太弱了,沒法動態更新UI。因此,Activity/Fragment也要擔當起一部分View層的責任,負責動態更新視圖。這就致使Activity/Fragment既有更新View控件的代碼,又有對源自Model的數據進行進一步邏輯處理的代碼,隨着迭代開發,Activity/Fragment會顯得愈來愈臃腫,分分鐘幾千行代碼,給後期維護帶來極大的困擾。若是你接手這樣的代碼,內心恐怕會不停讚歎前人真牛逼!mvc
不少博客,描述MVC時,都是這樣劃分MVC的,並指出:「View層和Model層是相互可知的,這意味着兩層之間存在耦合」。
我我的以爲這話有謬誤。View層和Model層必定是互相可知的?MVC的交互圖,V與M的箭頭,只表明它們能夠互相給對方發送消息。但並不意味着它們之間就必定互相可知。
若是View層和Model層是相互可知的,這也就意味着它們互相持有對方的引用,是經過對方的引用來給彼此發送消息。這確實意味着兩層之間存在耦合。下面是這種狀況下,View和Model交互的代碼。
class XActivity {
public void attachModel() {
VersionModel model = new VersionModel(this);
model.checkUpdate();
}
public void onResult(String result) {
System.out.println(this.getClass().getSimpleName() + "收到了迴應:" + result);
}
}
class VersionModel {
private XActivity mActivity;
//注意:activity的引用經過構造方法傳遞了進來
public VersionModel(XActivity activity) {
mActivity = activity;
}
public void checkUpdate() {
System.out.println("檢查更新!");
//用activity的引用傳遞更新信息。這樣,該方法就會綁定死了這個activity。
mActivity.onResult("暫無更新!");
}
}
public class Couple {
public static void main(String[] args) {
//這個VersionModel只能提供給XActivity這個界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
}
}/* Output:
檢查更新!
XActivity收到了迴應:暫無更新!
*/
複製代碼
但這樣的代碼,實在是太糟糕了。由於Model層的代碼,複用性是頗有必要的。好比,這個檢查版本更新的Model,也許除了閃屏頁須要,你的設置界面也須要有這個功能。像上面這樣寫,那你的Model和View綁定死了。因此,更恰當的作法,是下面這樣。Model的數據處理結果,經過接口回調給View,保證Model的複用性。
class XActivity {
public void attachModel() {
//跟前面不一樣,再也不是直接傳遞自身引用給Model,而是傳一個回調接口,經過回調接口,獲取數據處理結果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(XActivity.this.getClass().getSimpleName() + "收到了迴應:" + result);
}
});
model.checkUpdate();
}
}
class YActivity {
public void attachModel() {
//跟前面不一樣,再也不是直接傳遞自身引用給Model,而是傳一個回調接口,經過回調接口,獲取數據處理結果
VersionModel model = new VersionModel(new Callback() {
@Override
public void onResult(String result) {
System.out.println(YActivity.this.getClass().getSimpleName() + "收到了迴應:" + result);
}
});
model.checkUpdate();
}
}
interface Callback {
void onResult(String result);
}
class VersionModel {
private Callback callback;
//再也不是直接接受某個Activity實例作參數,這樣就不會再與某個Activity過於耦合,提升了複用性
public VersionModel(Callback callback) {
this.callback = callback;
}
public void checkUpdate() {
System.out.println("檢查更新!");
//用的是回調接口,傳遞更新信息。該方法,不會再是綁定死某個activity。
callback.onResult("暫無更新!");
}
}
public class Decouple {
public static void main(String[] args) {
//這個VersionModel能夠隨意提供給N個界面使用。
XActivity xActivity = new XActivity();
xActivity.attachModel();
YActivity yActivity = new YActivity();
yActivity.attachModel();
}
}/* Output:
檢查更新!
XActivity收到了迴應:暫無更新!
檢查更新!
YActivity收到了迴應:暫無更新!
*/
複製代碼
(二)View:"xml+Activity/Fragment"視爲View層,僅僅負責控件更新。Controller:將Activity/Fragment中的邏輯控制,包括對源自Model層的數據的進一步處理的操做,抽取出來,到相似XxxController這樣命名的類裏,做爲Controller。做爲Controller。Model:數據處理。
這是我本身瞎琢磨的一種劃分方法。
若是這樣劃分,能夠極大地減輕了View層的負擔。可是,如今業務邏輯處理都被抽調到了Controller。而當Controller須要根據Model的數據處理結果,來決定View下一步作什麼的時候,只能經過View去得到(由於View層和Model層能夠互相交互。而Controller只能單方向跟Model發消息)。而View自身已經不負責業務邏輯處理了,還得夾在它們中間,簡直畫蛇添足。
MVP的寫法,其實就是基於這種劃分的。但使用MVP來實現,不會有這種尷尬。
若是MVC改一改:Controller更名爲Presenter,View和Model再也不容許交互,Controller(Presenter)和Model之間的單向通信,改成雙向。那麼這種作法,就是所謂的MVP了。
Google開源的MVP架構示例項目:android-architecture,也是基於這種劃分:
View:xml+Activity/Fragment。
Presenter:根據業務邏輯,對Model得到的結果進一步處理,最後決定View何時更新UI。
Model:數據處理。
以下圖:
android-architecture這個項目有不少分支,本文主要參考的是todo-mvp、todo-mvp-rxjava、todo-mvp-dagger三個分支。這幾個分支對MVP的劃分是一致的,只是實現細節上的差異。想學習rxjava2和dagger2的,推薦翻閱這兩個分支。
MVP的運做過程,大體能夠這麼理解:View給Presenter指派任務,而後Presenter調控一個或多個Model,給它們劃分各類小任務,配合完成任務。最後,Model經過回調接口,告訴Presenter任務結果,而後Presenter根據任務的完成狀況,通知View更新UI。
以前用於mvc討論的檢查更新功能,若是用MVP寫。關鍵的代碼以下:
public interface SplashContract {
interface View{
void showUpdateDialog(VersionBean versionBean);
void jumpToMain();
}
interface Presenter{
void attachView(SplashContract.View view);
void detachView();
void checkUpdate();
}
}
public interface VersionCallback{
void onUpdate(VersionBean versionBean);
void onNoUpdate();
}
public class VersionBean{
private int versionCode;
private String versionName;
private String updateUrl;
public VersionBean(int versionCode,String versionName,String updateUrl){
this.versionCode = versionCode;
this.versionName = versionName;
this.updateUrl = updateUrl;
}
//省略get/set方法
......
}
public class SplashActivity extends AppCompatActivity implements SplashContract.View{
private SplashContract.Presenter mPresenter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
attachPresenter();
checkUpdate();
}
public void attachPresenter(){
//注意,使用的是Application的Context
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
mPresenter.attachView(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.detachView();
}
public void checkUpdate(){
mPresenter.checkUpdate();
}
@Override
public void showUpdateDialog(VersionBean versionBean){
//彈更新提示dialog
}
@Override
public void jumpToMain(){
//跳轉到MainActivity
}
}
public class SplashPresenter implements SplashContract.Presenter{
private VersionModel mVersionModel;
private SplashContract.View mView;
public SplashPresenter(VersionModel versionModel){
mVersionModel = versionModel;
}
@Override
public void attachView(SplashContract.View view){
mView = view;
}
public void onDetachView() {
//能夠在此取消全部的異步訂閱
}
@Override
public void checkUpdate(){
mVersionModel.checkUpdate(new VersionCallback(){
@Override
public void onUpdate(VersionBean versionBean){
//異步任務回來後,先判斷View是否處於正確的狀態
if(mView != null){
mView.showUpdateDialog(versionBean);
}
}
@Override
public void onNoUpdate(){
//異步任務回來後,先判斷View是否處於正確的狀態
if(mView != null){
mView.jumpToMain();
}
}
});
}
}
public class VersionModel{
private Context mContext;
public VersionModel(Context context){
mContext = context;
}
public void checkUpdate(VersionCallback callback){
//經過網絡獲取更新信息
VersionBean versionBean = fromServer();
if(BuildConfig.VERSION_CODE < versionBean.getVersionCode()){
callback.onUpdate(versionBean);
}else{
callback.onNoUpdate();
}
}
}
複製代碼
區區一個檢測更新而已,這代碼量會不會有點多了?
基於todo-mvp的mvp寫法,我的概括的注意事項以下:
你能夠理解爲,View是很懶很懶的,不肯意動腦子,幾乎全部的邏輯處理,哪怕只是簡單的「1+1=?」的問題,都推給Presenter。我以爲,除了實現視圖和邏輯的分離,還有一部分緣由是:若是是Presenter太臃腫,你徹底能夠根據功能不一樣,輕易拆分紅兩個甚至更多的Presenter。可是若是Activity/Fragment(View)太臃腫的話,可能就很差拆分了。
因此,View只負責初始化自身,根據須要,給Presenter指派任務,而後進入「瞌睡」狀態。只有兩種狀況纔會被從新喚醒:
1)當接受來自外界的刺激時,好比:點擊事件;
2)當Presenter有處理返回:嗨,孫子,醒醒,起來倒茶了(更新UI)。
下面是接口寫法的示例。
public interface XxxContract {
interface View{
}
interface Presenter{
}
}
複製代碼
Activity/Fragment實現XxxContract.View接口,Presenter實現XxxContract .Presenter接口。二者在使用對方的實例時,一般都會向上轉型爲對應的接口,也就是說,暴露給對方的方法,都會寫在接口裏面。
其實,縱觀谷歌的demo,View和Presenter之間的耦合度一般都是很高的,Presenter是爲對應的View量身定製的,複用的可能性不大,一般也沒有這個必要。
而這個接口,如其名,更像是一個契約接口。裏面兩個關鍵的子接口,在這種狀況下,徹底失去了接口存在的最大意義:"多繼承"。
「肯定接口是理想選擇,於是應該老是選擇接口而不是具體的類。」這實際上是一種引誘。固然,對於建立類,幾乎在任什麼時候刻,均可以替代爲建立一個接口和一個工廠。許多人都掉進了這種陷阱,只要有可能就去建立接口和工廠。這種邏輯看起來好像是由於須要使用不一樣的具體實現,所以老是應該添加這種抽象性。這實際上已經變成了一種草率的設計優化。任何抽象性都應該是應真正的需求而產生的。若是沒有足夠的說服力,只是爲了以防萬一添加新接口,並由此帶來了額外的複雜性。那麼應該質疑一下這樣的設計了。恰當的原則,應該是優先選擇類而不是接口。接口是一種重要工具,但它們容易被濫用。」 ——摘自《Java編程思想》第9章 接口 第188-189頁
尤爲是MVP用得多了,這個契約接口,總讓人感到彆扭。好比,想在Presenter添加一個方法,除了在Presenter裏寫它一次,還得在契約接口裏面聲明一次。它給我帶來的麻煩,彷佛多於給個人便利。
這個Contract接口有沒有存在的必要,真的有待商榷。
因此,我的更傾向於:去除Contract接口、Presenter接口,只保留View接口。Activity/Fragment則直接使用Presenter的實例時,再也不將其向上轉型爲接口。在調用Presenter的attachView方法的時候,將Activigty/Fragment的實例向上轉型爲接口,避免Presenter濫用該實例引用。
以上僅是我的觀點,也許鄙人目光短淺,有所謬誤。歡迎指正。
由於Presenter的生命週期,一般與Activity/Fragment是不相同的。因此Presenter在執行異步操做後,在結束的時候,都要判斷View是否還處於正確的狀態。這樣,就能有效得避免了在異步任務完成時,Activity/Fragment卻已經被銷燬而致使的空指針等問題。固然,同步任務一般不須要這種判斷。
有趣的是,todo-mvp、todo-mvp-dagger、todo-mvp-rxjava這三個分支對上述操做的作法都不一樣,但有殊途同歸之妙。下面給出它們作法的關鍵代碼。
todo-mvp
public class TasksFragment extends Fragment implements TasksContract.View {
/**
* Return true if the fragment is currently added to its activity.
*/
@Override
public boolean isActive() {
return isAdded();
}
}
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//異步任務
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//異步任務結束後,再調View以前,必須先判斷
//不過這種作法,其實只適合實現View接口的是Fragment的時候
if (!mTasksView.isActive()) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
}
複製代碼
todo-mvp-dagger
final class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//異步任務
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
//異步任務結束後,再調View以前,必須先判斷
if (mTasksView == null) {
return;
}
mTasksView.setLoadingIndicator(false);
}
});
}
//在View解除綁定時,會被調用(須要你本身在onDestroy/onDestroyView裏面手動調用)
@Override
public void dropView() {
mTasksView = null;
}
}
複製代碼
todo-mvp-rxjava
public class TasksPresenter implements TasksContract.Presenter {
@Override
public void loadTasks(boolean forceUpdate) {
//異步任務
Disposable disposable = mTasksRepository
.getTasks()
.subscribe(tasks -> mTasksView.setLoadingIndicator(false));
//將能取消異步任務的disposable添加進CompositeDisposable
mCompositeDisposable.add(disposable);
}
//在View解除綁定時,會被調用(須要你本身在onDestroy/onDestroyView裏面手動調用)
// 取消mCompositeDisposable裏面添加的全部RxJava的異步任務。
@Override
public void unsubscribe() {
mCompositeDisposable.clear();
}
}
複製代碼
就像我上面講MVC時舉的例子,Model不該該持有Presenter的引用,它不該該知道會是哪一個Presenter來調用它。它的數據處理結果,都應該經過回調接口來交給調用它的Presenter。若是,你以爲寫這些回調接口很煩,那麼你能夠考慮學習一波RxJava了。
反面示例:
//不該該將Presenter的引用傳給Model
public MainModel(MainContract.Presenter mainPresenter) {
this.mainPresenter= mainPresenter;
}
複製代碼
這是個很典型的反面例子...我也曾經這麼寫過Model的代碼,直到有一天,我須要在其餘地方複用某個Model的時候...笑哭.jpg。
緣由以下:
1)避免Context在子線程使用時,因爲Activity忽然被銷燬,致使的空指針。
2)避免本身偷懶,在Presenter和Model執行更新UI的操做。實際上,這也是很不該該的操做。我在不少項目裏面,常常看到這樣的錯誤操做:在Presenter裏更新UI、把Adapter的代碼放進Presenter裏面等等。MVP的劃分,不只僅是爲了代碼的解耦、複用,也有很大一部分緣由是由於Android類不能直接在JVM上運行,會影響咱們寫單元測試。而儘可能隔離、減小使用Android類的Presenter、Model層,會更便於咱們寫相關單元測試。
3)Application的Context足夠應付Presenter和Model裏面的需求了。看下錶,Context的應用場景:
*數字1:啓動Activity在這些類中是能夠的,可是須要建立一個新的task。通常狀況不推薦。
*數字2:在這些類中去layout inflate是合法的,可是會使用系統默認的主題樣式,若是你自定義了某些樣式可能不會被使用。
*數字3:在receiver爲null時容許,在4.2或以上的版本中,用於獲取黏性廣播的當前值。(能夠無視) ContentProvider、BroadcastReceiver之因此在上述表格中,是由於在其內部方法中都有一個context用於使用。
你能夠看見,Activity的Context能作的,Application的Context基本都能作。若是你非要在Presenter、Model裏面使用Activity的Context,我想你更應該考慮是否是該把這段代碼放到Activity/Fragment裏面。
所謂的依賴注入,就是A類裏面須要使用到B類,而B類的實例,在一開始,就先在外面建立好,而後經過A類的構造方法傳遞進來的。先前的MVP示例裏的這段代碼,就完成了一個簡單的依賴注入。
VersionModel versionModel = new VersionModel(getApplicationContext());
mPresenter = new SplashPresenter(versionModel);
複製代碼
如今流行的Dagger2這個依賴注入框架只是能極大地簡化了你本身去手動new這些實例,甚至建立這些實例的單例,再注入的過程。
全局的Context,若是要在Presenter、Model裏面使用,也要經過構造方法,將Context傳遞進來。很大的一部分緣由也是:方便寫單元測試代碼。
由於單元測試裏面有一個mock的概念。依賴注入,能方便你mock相應的類。若是對單元測試有興趣的話,到時能夠參考個人相關測試文章。
代碼示例: todo-mvp裏的某個Presenter構造方法的部分代碼
public TasksPresenter(@NonNull TasksRepository tasksRepository) {
/**
* 至關於mTasksRepository = tasksRepository,並檢測tasksRepository是否爲null。
* 若是爲空,拋空指針異常。
*/
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
}
複製代碼
注:checkNotNull是谷歌的guava裏面的方法。雖然谷歌demo的各個分支,都有用到guava庫,一個工具類庫。但實際項目中並不推薦適用它,由於android裏64k方法的限制。這個庫它有1w+方法...若是你喜歡用相關方法,建議把相關類複製出來便可。
前面提到,Model是「數據處理」,Presenter是「根據業務邏輯,對Model得到的結果進一步處理」。不少人也許會對Presenter、Model的劃分產生疑問:一樣都會進行數據處理,怎麼劃分才更恰當?
其實MVP三層的關係,從另外一個角度看的話,是層層遞進的關係,以下圖。
而前面咱們已經知道Model的複用性是很重要。假設,若是有多個Presenter都對Model中得到的數據,進行一樣的處理,那麼咱們就應該考慮,把這一段處理代碼,「下沉」到Model層裏。若是Model裏的一個方法,有一段代碼只針對某個Presenter作了特殊處理,那麼咱們就應該考慮,把這段處理代碼,「上浮」到那個特定的Presenter裏。
相比MVC的各類尷尬,MVP之間的分工合做明顯更加合理,便於維護、測試。箇中好處,只能你理解和熟練使用MVP後,才能深有感觸。
建議認真翻閱谷歌的官方架構demo:android-architecture
我我的也提供了一個用MVP寫的訊飛語音交互demo:TalkDemo
若是你對dagger、rxjava熟悉,也能夠參閱個人另外一個基於mvp+dagger2(dagger.android)+rxjava2+retrofit2的架構demo:ArchitectureDemo
以上內容,均基於我的的理解和谷歌的架構demo總結的。
若有謬誤,請及時提醒,不勝感激!