Android MVP架構和MVC架構比較
代碼示例請點擊點擊下載demo
1.概述 如題,本文想要討論的是MVP與MVC之間的比較,那麼在這之前,我們首先來回顧一下MVC的概念.MVC我們再熟悉不過,即Model-View-Controllor,對應於Android項目結構如下
- Model對應於業務邏輯和實體類
- View對應於xml佈局文件
- Controller對應於Activity
- 1
- 2
- 3
- 4
這裏我們的View佈局文件只做了界面的展示工作,做的工作其實並不多,當然現在Android有了MVVM來解決這個問題,讓View變的更加強大,我們這裏暫且不做討論.而界面的大部分顯示邏輯和數據處理邏輯都是在Activity中完成的(類似於ios中的ViewControllor類),這就使我們的Activity即像Controllor又像View,導致Activity中的代碼變的異常複雜,我本人以前接手的一個項目MainActivity中有超過3000行的代碼,導致後期根本無法維護,只能重寫來解決.
而隨着MVP架構的出現,我們可以將以往在Activity中完成的數據展示邏輯到Presenter中完成,對應的架構就變成了下邊這樣
- Model不變對應於業務邏輯和實體類
- View對應於Activity,負責界面的展示
- Presenter,負責業務邏輯處理,來連接Model和View
- 1
- 2
- 3
- 4
2.MVP與MVC對比
下邊用一張圖來更清晰的對比兩種邏輯
可以看到,二者最明顯的區別就是,MVC允許View和Model進行數據交互,而MVP則是把View和Model的交互交給Presenter來完成,這樣就使得Activity中的變的更輕,代碼更加清爽簡潔,耦合度也更低,便於後期的維護.
通過代碼來展示二者在設計上的一些區別
3.代碼設計
1.包結構 這裏通過一個模擬登錄的例子來演示二者在設計上的異同,首先我們設計一下程序的包結構,一個好的包結構是程序的基石.貼出一張圖來展示一下我的Demo程序包結構
是的,看上去很簡單,就是model-view-presenter,讓人一目瞭然.接下來我們來分析一下具體的業務邏輯,並且來編寫代碼.
1.Action層 首先是實體類,這個必不可少,不做討論
/**
* 登錄返回用戶信息
* Created by shidong on 15/11/26.
*/
public class UserInfo implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
2.Action中的登錄操作,我們就叫login()方法
/**
* 具體實現登錄操作(項目中一般是網絡請求返回數據)
* Created by shidong on 15/11/26.
*/
public class LoginAction {
public UserInfo login() {
UserInfo userInfo = new UserInfo();
userInfo.setName("dong");
userInfo.setAge(20);
return userInfo;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
好了,到這裏我們的action層已經編寫完了,跟以往的寫法沒有半點區別.
2.Presenter層
可以試想一下,我們的登錄操作通常會有什麼呢,無非就是在異步線程中請求登錄操作,即上邊的LoginAction,然後處理登錄成功和失敗的邏輯.那麼好,我們的登錄功能接口定義大概就是下邊這樣子
/**
* 登錄接口
* Created by shidong on 15/11/26.
*/
public interface OnLoginListener {
//登錄成功
public void loginSuccess(UserInfo userInfo);
//登錄失敗
public void loginFailure();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
爲了演示方便,暫且只定義這兩個方法.
現在應該設計我們的Presenter類了,嗯,很顯然,我們需要有一個login()方法,這個login調用LoginAction中的方法完成登錄請求.應該是這樣的
public void login() {
request(REQ_LOGIN);
}
- 1
- 2
- 3
嗯?request()方法是什麼東東,好了,接下來繼續分析.別忘了,這些請求一定是在異步線程中實現的,耗時操作都是需要這個異步請求操作的,Android已經爲我們提供了AsyncTask,我們來對其進一步封裝.既然是公用的,我們需要定義一個接口來完成上述操作,接口中方法可以仿照AsyncTask類設計如下
/**
* [異步請求監聽]
*
* @author mashidong
* @version V3.6.0
* @date 2015-11-25
*
**/
public interface OnDataListener {
/**
* 耗時操作將在該方法中實現,比如髮網絡請求等
* @param requestCode 請求code
* @return
* @throws HttpException
*/
public Object doInBackground(int requestCode) throws HttpException;
/**
* 請求成功回調方法
* @param requestCode 請求code
* @param result 結果
*/
public void onSuccess(int requestCode, Object result);
/**
* 請求失敗回調方法
* @param requestCode 請求code
* @param state 狀態
* @param result 結果
*/
public void onFailure(int requestCode, int state, Object result);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
之後我們的每個Presenter都需要實現這個接口來完成異步請求任務,誰都不想每次都去寫重複的代碼,顯然我們需要定義一個BasePresenter來完成這些操作.
package com.example.shidong.androidmvp.presenter;
import com.example.shidong.DemoApplication;
import com.example.shidong.network.async.AsyncTaskManager;
import com.example.shidong.network.async.OnDataListener;
import com.example.shidong.network.http.HttpException;
import com.example.shidong.network.utils.NToast;
/**
* Presenter 基礎類,提供異步請求,設置監聽器方法等
* <p>
* Created by shidong on 15/11/26.
*/
public abstract class BasePresenter<T> implements OnDataListener {
private AsyncTaskManager mAsyncTaskManager;
public BasePresenter() {
//構造方法中初始化AsyncTaskManager
this.mAsyncTaskManager = AsyncTaskManager.getInstance(DemoApplication.application);
}
/**
* 發送請求,默認是需要檢查網絡的
*
* @param requsetCode 請求code
*/
public void request(int requsetCode) {
request(requsetCode, true);
}
/**
* 發送請求
*
* @param requsetCode 請求code
* @param isCheckNetwork 是否需要檢查網絡, true需要,false不需要
*/
public void request(int requsetCode, boolean isCheckNetwork) {
mAsyncTaskManager.request(requsetCode, isCheckNetwork, this);
}
/**
* 取消請求
*
* @param requsetCode
*/
public void cancelRequest(int requsetCode) {
mAsyncTaskManager.cancelRequest(requsetCode);
}
/**
* 取消所有請求
*/
public void cancelRequest() {
mAsyncTaskManager.cancelRequest();
}
/**
* 設置listener
*
* @param listener
*/
public abstract void setListener(T listener);
@Override
public Object doInBackground(int requestCode) throws HttpException {
return null;
}
@Override
public void onSuccess(int requestCode, Object result) {
}
@Override
public void onFailure(int requestCode, int state, Object result) {
switch (state) {
// 網絡不可用給出提示
case AsyncTaskManager.HTTP_NULL_CODE:
NToast.shortToast(DemoApplication.application, "網絡不可用");
break;
// 網絡有問題給出提示
case AsyncTaskManager.REQUEST_ERROR_CODE:
break;
case AsyncTaskManager.HTTP_ERROR_CODE:
NToast.longToast(DemoApplication.application, "網絡連接錯誤");
break;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
其中我們定義一個AsyncTaskManager來對異步請求進行管理.具體見代碼
package com.example.shidong.network.async;
import android.content.Context;
import android.os.Build;
import com.example.shidong.network.utils.NLog;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 異步框架類,該類主要實現將耗時操作放在異步線程裏面操作,如頁面發送請求等。
* <p/>
* 發生請求成功: REQUEST_SUCCESS_CODE = 200
* 發生請求失敗: REQUEST_ERROR_CODE = -999
* 網絡有問題: HTTP_ERROR_CODE = -200
* 網絡不可用: HTTP_NULL_CODE = -400
*
* @author mashidong
* @version V3.6.0
* @date 2015-11-25
**/
public class AsyncTaskManager {
/**
* 日誌對象
**/
private final String tag = AsyncTaskManager.class.getSimpleName();
/**
* 發生請求成功
**/
public static final int REQUEST_SUCCESS_CODE = 200;
/**
* 發生請求失敗
**/
public static final int REQUEST_ERROR_CODE = -999;
/**
* 網絡有問題
**/
public static final int HTTP_ERROR_CODE = -200;
/**
* 網絡不可用
**/
public static final int HTTP_NULL_CODE = -400;
/**
* 默認下載請求碼
**/
public static final int DEFAULT_DOWNLOAD_CODE = 10000;
/**
* 線程池最多線程數
**/
public final int MAX_CONNECTIONS_NUM = 10;
private Context mContext;
private static AsyncTaskManager instance;
private static ExecutorService mExecutorService;
private static Map<Integer, WeakReference<BaseAsyncTask>> requestMap;
/**
* 構造方法
*
* @param context
*/
private AsyncTaskManager(Context context) {
mContext = context;
mExecutorService = Executors.newFixedThreadPool(MAX_CONNECTIONS_NUM);
requestMap = new WeakHashMap<Integer, WeakReference<BaseAsyncTask>>();
}
/**
* 單例模式得到AsyncTaskManager實例對象
*
* @param context
* @return
*/
public static AsyncTaskManager getInstance(Context context) {
if (instance == null) {
synchronized (AsyncTaskManager.class) {
if (instance == null) {
instance = new AsyncTaskManager(context);
}
}
}
return instance;
}
/**
* 發送請求, 默認是需要檢查網絡的
*
* @param requestCode 請求code
* @param listener回調
*/
public void request(int requestCode, OnDataListener listener) {
request(requestCode, true, listener);
}
/**
* 發送請求
*
* @param requestCode 請求code
* @param isCheckNetwork 是否需要檢查網絡
* @param listener 回調
*/
public void request(int requestCode, boolean isCheckNetwork, OnDataListener listener) {
DownLoad bean = new DownLoad(requestCode, isCheckNetwork, listener);
if (requestCode > 0) {
BaseAsyncTask mAsynctask = new BaseAsyncTask(bean, mContext);
//after version 2.3 added executeOnExecutor method.
//before 2.3 only run five asyntask, more than five must wait
if (Build.VERSION.SDK_INT >= 11) {
mAsynctask.executeOnExecutor(mExecutorService);
} else {
mAsynctask.execute();
}
requestMap.put(requestCode, new WeakReference<BaseAsyncTask>(mAsynctask));
} else {
NLog.e(tag, "the error is requestCode < 0");
}
}
/**
* 根據requestCode取消請求
*
* @param requestCode
*/
public void cancelRequest(int requestCode) {
WeakReference<BaseAsyncTask> requestTask = requestMap.get(requestCode);
if (requestTask != null) {
BaseAsyncTask request = requestTask.get();
if (request != null) {
request.cancel(true);
request = null;
}
}
requestMap.remove(requestCode);
}
/**
* 取消所有請求
*/
public void cancelRequest() {
if (requestMap != null) {
Iterator<Entry<Integer, WeakReference<BaseAsyncTask>>> it = requestMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Integer, WeakReference<BaseAsyncTask>> entry = (Entry<Integer, WeakReference<BaseAsyncTask>>) it.next();
Integer requestCode = entry.getKey();
cancelRequest(requestCode);
}
requestMap.clear();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
這回好了,異步請求方法有了,我們就大膽的在Presenter中完成我們的操作吧.
package com.example.shidong.androidmvp.presenter;
import com.example.shidong.androidmvp.presenter.impl.OnLoginListener;
import com.example.shidong.androidmvp.module.action.LoginAction;
import com.example.shidong.androidmvp.module.model.UserInfo;
import com.example.shidong.network.http.HttpException;
/**
* 登錄Presenter 實現登錄邏輯
* Created by shidong on 15/11/26.
*/
public class LoginPresenter extends BasePresenter<OnLoginListener> {
private static final int REQ_LOGIN = 0x0a;
private OnLoginListener loginListener;
@Override
public void setListener(OnLoginListener listener) {
this.loginListener = listener;
}
public void login() {
request(REQ_LOGIN);
}
@Override
public Object doInBackground(int requestCode) throws HttpException {
switch (requestCode) {
case REQ_LOGIN:
LoginAction action = new LoginAction();
return action.login();
default:
return null;
}
}
@Override
public void onSuccess(int requestCode, Object result) {
super.onSuccess(requestCode, result);
switch (requestCode) {
case REQ_LOGIN:
if (result != null) {
UserInfo userInfo = (UserInfo) result;
if (loginListener != null) {
loginListener.loginSuccess(userInfo);
}
}
default:
break;
}
}
@Override
public void onFailure(int requestCode, int state, Object result) {
super.onFailure(requestCode, state, result);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
ok,到這裏,我們所有的異步請求邏輯已經完成的,好像還少點什麼,不錯,最後要在我們的View中調用請求方法來完成顯示
3.Activity設計
package com.example.shidong.androidmvp.view;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import com.example.shidong.androidmvp.R;
import com.example.shidong.androidmvp.presenter.impl.OnLoginListener;
import com.example.shidong.androidmvp.module.model.UserInfo;
import com.example.shidong.androidmvp.presenter.LoginPresenter;
import com.example.shidong.network.utils.NToast;
public class LoginActivity extends AppCompatActivity implements OnLoginListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_mvp);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//MVP模式
findViewById(R.id.btn_login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//登錄請求
LoginPresenter presenter = new LoginPresenter();
presenter.setListener(LoginActivity.this);
presenter.login();
}
});
//MVC模式
findViewById(R.id.btn_test_mvc).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LoginActivity.this.startActivity(new Intent(LoginActivity.this, MVCLoginActivity.class));
}
});
}
//登錄成功
@Override
public void loginSuccess(UserInfo userInfo) {
NToast.longToast(this, "登錄成功" + userInfo.toString());
}
@Override
public void loginFailure() {
NToast.longToast(this, "登錄失敗");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
至此,我們的整個流程已經完成,Activity中只有區區幾十行代碼,處理幾個回調方法即可,是不是很清爽,而且所有的邏輯都做了很好的分層,便於代碼閱讀和後期維護.當然這種寫法帶來的代價就是會增加很多類和接口的定義,所以我本人建議適當使用,即能用則用,簡單的邏輯可以不用.比如上述登錄請求,直接用MVC來完成是下邊這樣
package com.example.shidong.androidmvp.view;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import com.example.shidong.androidmvp.R;
import com.example.shidong.androidmvp.module.action.LoginAction;
import com.example.shidong.androidmvp.module.model.UserInfo;
import com.example.shidong.network.async.AsyncTaskManager;
import com.example.shidong.network.async.OnDataListener;
import com.example.shidong.network.http.HttpException;
import com.example.shidong.network.utils.NToast;
public class MVCLoginActivity extends AppCompatActivity implements OnDataListener {
private static final int REQ_MVCLOGIN = 0x0b;
private AsyncTaskManager mAsyncTaskManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvclogin);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mAsyncTaskManager = AsyncTaskManager.getInstance(this);
findViewById(R.id.btn_mvclogin).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
request(REQ_MVCLOGIN);
}
});
}
/**
* 發送請求,默認是需要檢查網絡的
*
* @param requsetCode 請求code
*/
public void request(int requsetCode) {
request(requsetCode, true);
}
/**
* 發送請求
*
* @param requsetCode 請求code
* @param isCheckNetwork 是否需要檢查網絡, true需要,false不需要
*/
public void request(int requsetCode, boolean isCheckNetwork) {
mAsyncTaskManager.request(requsetCode, isCheckNetwork, this);
}
/**
* 取消請求
*
* @param requsetCode
*/
public void cancelRequest(int requsetCode) {
mAsyncTaskManager.cancelRequest(requsetCode);
}
/**
* 取消所有請求
*/
public void cancelRequest() {
mAsyncTaskManager.cancelRequest();
}
@Override
public Object doInBackground(int requestCode) throws HttpException {
LoginAction action = new LoginAction();
return action.login();
}
@Override
public void onSuccess(int requestCode, Object result) {
UserInfo userInfo = (UserInfo) result;
NToast.longToast(this, userInfo.toString());
}
@Override
public void onFailure(int requestCode, int state, Object result) {
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
所有的請求和顯示邏輯在Activity中完成,目測比MVP寫法多了一倍的代碼,還好我們已經將異步請求邏輯封裝,否則可能會更多;
這樣兩種方法我們都可以運用自如, 在適當的時候選擇適當的方法來完成.
好了,到這裏已經寫完我想要寫的東西,個人水平有限,可能理解不夠深刻,望多多討論和提出更好建議.最後感謝hongyang大神的文章,讓我進一步完善了項目總的請求封裝邏輯.
參考文章 http://blog.csdn.net/lmj623565791/article/details/46596109
完整Demo代碼請點擊 http://download.csdn.net/detail/ronaldong99/9303337