MVP
簡(jiǎn)介
MVP是模型(Model)、視圖(View)碍岔、主持人(Presenter)的縮寫浴讯,分別代表項(xiàng)目中3個(gè)不同的模塊。如圖所示:
- View 對(duì)應(yīng)于Activity蔼啦、Fragment榆纽,負(fù)責(zé)界面的繪制以及與用戶交互
- Model 依然是業(yè)務(wù)邏輯和實(shí)體模型
- Presenter 負(fù)責(zé)完成View于Model間的交互
設(shè)計(jì)前思考:
- 首先在我們常用的MVC模式中,Activity承載了太多捏肢,做了不只是視圖層的事情掠河,而程序開發(fā)中最重要的 Context 一般也是在視圖層才擁有的,所以我們需要把Context保持在視圖中猛计。
- MVP相對(duì)于MVC唠摹,MVP中是依賴Presenter這個(gè)接口任務(wù)調(diào)度器來實(shí)現(xiàn)任務(wù)調(diào)度,則視圖層中所有需要進(jìn)行數(shù)據(jù)交互的奉瘤,都需要將數(shù)據(jù)交給Presenter勾拉,而Presenter將調(diào)用Model來加載數(shù)據(jù)。
- 在傳統(tǒng)的MVC中盗温,我常用 initView()藕赞、initData()、initEvent()卖局、doOther() 這幾個(gè)方法來實(shí)現(xiàn)數(shù)據(jù)流程加載斧蜕、界面交互實(shí)現(xiàn)。現(xiàn)在我們需要拆分出來砚偶,Activity從BaseActivity中實(shí)現(xiàn)批销。
經(jīng)過這樣的構(gòu)思洒闸,我們可以先實(shí)踐一下,我們讓View來實(shí)現(xiàn)Model的接口均芽,View來調(diào)用presenter丘逸,presenter利用面向接口編程的思想來調(diào)用接口實(shí)現(xiàn)對(duì)View的操作。實(shí)例如下:
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.acheng.achengutils.mvp.model.BaseViewController;
import com.acheng.achengutils.mvp.presenter.BasePresenter;
/**
* Created by pc859107393 on 2016/6/28.
*/
public abstract class BaseActivity<T extends BasePresenter, M extends BaseViewController> extends AppCompatActivity {
public String TAG; //當(dāng)前Activity的標(biāo)記
protected T mPresenter; //主持人角色
protected abstract T initPresenter(); //獲取到主持人
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TAG = String.format("%s::%s", getPackageName(), getLocalClassName());
mPresenter = initPresenter(); //初始化Presenter掀宋,提供主持人深纲,擁有主持人后才能提交界面數(shù)據(jù)給presenter
setContentView(setLayoutId());
initView();
mPresenter.initData();
initEvent();
doOther();
}
protected void doOther() {
}
public Context getContext() {
return this;
}
protected abstract void initEvent();
protected abstract void initView();
protected abstract int setLayoutId();
@Override
protected void onResume() {
super.onResume();
//如果presenter為空的時(shí)候,我們需要重新初始化presenter
if (mPresenter == null) {
mPresenter = initPresenter();
}
}
@Override
protected void onPause() {
super.onPause();
}
@Override
public void onBackPressed() { //返回按鈕點(diǎn)擊事件
//當(dāng)Activity中的 進(jìn)度對(duì)話框正在旋轉(zhuǎn)的時(shí)候(數(shù)據(jù)正在加載劲妙,網(wǎng)絡(luò)延遲高湃鹊,數(shù)據(jù)難以加載),關(guān)閉 進(jìn)度對(duì)話框 , 然后可以手動(dòng)執(zhí)行重新加載
super.onBackPressed();
}
/**
* 恢復(fù)界面后,我們需要判斷我們的presenter是不是存在,不存在則重置presenter
*
* @param savedInstanceState
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (mPresenter == null)
mPresenter = initPresenter();
}
/**
* onDestroy中銷毀presenter
*/
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter = null;
}
}
既然我們的Activity已經(jīng)設(shè)定好了BaseActivity镣奋,我們需要接著完成BasePresenter涛舍,如下:
import com.acheng.achengutils.mvp.model.BaseViewController;
/**
* Created by acheng on 2016/7/14.
*/
public abstract class BasePresenter<D extends BaseViewController> {
public D model;
/**
* 在子類的構(gòu)造函數(shù)中,設(shè)定參數(shù)為model唆途,這時(shí)候可以presenter調(diào)用接口來實(shí)現(xiàn)對(duì)界面的操作富雅。
*/
public BasePresenter(D model) {
this.model = model;
}
public abstract void initData();
}
關(guān)于我這個(gè)Presenter的設(shè)計(jì),我想說的是我們需要將各層解耦肛搬,那么我的presenter就不應(yīng)該持有Android程序流轉(zhuǎn)的必然因子没佑,如Context、Bundle温赔、Intent蛤奢、View等,如果我們需要實(shí)現(xiàn)對(duì)界面的操作陶贼,必須通過調(diào)用我們?cè)O(shè)定好的Model來實(shí)現(xiàn)啤贩,關(guān)于BaseModel更加簡(jiǎn)單了,直接是一個(gè)空的接口文件拜秧,如下:
public interface BaseViewController {
//這里面添加實(shí)現(xiàn)類需要實(shí)現(xiàn)的方法即可
}
設(shè)計(jì)后的思考
- presenter作為主持人,應(yīng)該隨著視圖的關(guān)閉而關(guān)閉,所以我們需要在Activity和Fragment的關(guān)閉的時(shí)候,注銷相應(yīng)的presenter
- 在應(yīng)用程序被銷毀的時(shí)候,我們重啟了程序,但是這時(shí)應(yīng)用的狀態(tài)如果不恢復(fù)到前面的狀態(tài)那么我們需要把對(duì)應(yīng)的presenter重建
- 在應(yīng)用恢復(fù)后,如果想保持剛才的狀態(tài),那么我們需要在被銷毀前把視圖的狀態(tài)保存,并且恢復(fù)對(duì)應(yīng)的狀態(tài)
說了這么多痹屹,我們直接手底下見真章:
import android.Manifest;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.TextView;
import com.acheng.achengutils.mvp.view.BaseActivity;
import com.acheng.achengutils.utils.SPHelper;
import com.acheng.achengutils.widgets.AppUpdateDialog;
import com.acheng.achengutils.widgets.MustDoThingDailog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.R;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.view.activity.presenter.LoginActivityPresenter;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;
import acheng1314.cn.a3dbuild.widgets.MyProgressDialog;
/**
* Created by pc859107393 on 2016/9/12 0012.
*/
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {
private View mBt_login;
private TextView mEt_username; //用戶名
private TextView mEt_password; //密碼s
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
private AppUpdateDialog appPermission; //權(quán)限申請(qǐng)對(duì)話框
private MyProgressDialog myProgressDialog; //進(jìn)度對(duì)話框
@Override
protected LoginActivityPresenter initPresenter() {
return new LoginActivityPresenter(this); //實(shí)例化LoginActivity的Presenter
}
@Override
protected void initEvent() {
mBt_login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MyApplication.getInstance().outLog(TAG, "MDZZ"); //日志輸出
//調(diào)用Presenter的登錄的網(wǎng)絡(luò)請(qǐng)求,將用戶名和密碼傳遞過去
mPresenter.doLogin(mEt_username.getText().toString(), mEt_password.getText().toString());
}
});
}
@Override
protected void initView() {
MyApplication.getInstance().addActivity(this); //將Activity加入堆棧管理
mEt_username = (TextView) findViewById(R.id.mEt_username);
mEt_password = (TextView) findViewById(R.id.mEt_password);
mBt_login = findViewById(R.id.mBt_login);
}
@Override
protected void doOther() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
List<String> permissionsNeeded = new ArrayList<String>();
final List<String> permissionsList = new ArrayList<String>();
if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE))
permissionsNeeded.add("手機(jī)存儲(chǔ)空間");
if (!addPermission(permissionsList, Manifest.permission.READ_PHONE_STATE))
permissionsNeeded.add("獲取手機(jī)狀態(tài)");
if (!addPermission(permissionsList, Manifest.permission.CAMERA))
permissionsNeeded.add("手機(jī)相機(jī)");
if (!addPermission(permissionsList, Manifest.permission.ACCESS_COARSE_LOCATION))
permissionsNeeded.add("手機(jī)位置");
// if (!addPermission(permissionsList, Manifest.permission.WRITE_SETTINGS))
// permissionsNeeded.add("手機(jī)設(shè)置");
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) { //待申請(qǐng)的權(quán)限列表
// Need Rationale
String message = "你必須允許本APP使用:" + permissionsNeeded.get(0);
for (int i = 1; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
}
super.doOther();
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton("允許", okListener)
.setNegativeButton("拒絕", null)
.create()
.show();
}
private boolean addPermission(List<String> permissionsList, String permission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
Map<String, Integer> perms = new HashMap<String, Integer>();
// Initial
perms.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_PHONE_STATE, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.ACCESS_COARSE_LOCATION, PackageManager.PERMISSION_GRANTED);
// Fill with results
for (int i = 0; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// Check for ACCESS_FINE_LOCATION
if (perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
//經(jīng)過用戶授權(quán)枉氮,獲得所有權(quán)限
if (appPermission != null) {
appPermission = null;
}
// All Permissions Granted
} else { //未得到用戶授權(quán)
// Permission Denied
appPermission = new AppUpdateDialog(AppUpdateDialog.IMPORTANT, "一些權(quán)限未被允許志衍,請(qǐng)?jiān)谠O(shè)置中授權(quán)!", getContext(), new AppUpdateDialog.NeedDoThing() {
@Override
public void mustDoThing() {
Uri packageURI = Uri.parse("package:" + getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
startActivity(intent);
}
});
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override
protected void onResume() {
super.onResume();
doOther();
}
@Override
protected int setLayoutId() {
return R.layout.activity_login;
}
@Override
public void showDailog(String msg) {
new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
@Override
public void mustDoThings() {
}
});
}
@Override
public void showProgressD() {
if (null == myProgressDialog)
myProgressDialog = new MyProgressDialog("登陸", "正在登錄···", getContext());
else
myProgressDialog.show();
}
@Override
public void disProgressD() {
if (null != myProgressDialog)
myProgressDialog.dismiss();
}
@Override
public void openHome(LoginBean bean) {
SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.username), mEt_username.getText().toString());
SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.password), mEt_password.getText().toString());
SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.userId), bean.getResult().getUserId());
SPHelper.setString(getContext(), getContext().getString(R.string.user), getContext().getString(R.string.token), bean.getResult().getToken());
startActivity(new Intent(getContext(), HomeActivity.class));
finish();
}
}
其實(shí)上面我們當(dāng)中可以看到我們前臺(tái)界面拿到用戶數(shù)據(jù)后,調(diào)用presenter的doLogin方法聊替,把用戶名和密碼傳遞過去楼肪,然后我們?cè)赑resenter中請(qǐng)求網(wǎng)絡(luò)然后再通過調(diào)用接口實(shí)現(xiàn)數(shù)據(jù)回傳。如下:
import com.acheng.achengutils.gsonutil.GsonUtils;
import com.acheng.achengutils.mvp.presenter.BasePresenter;
import com.acheng.achengutils.utils.CipherUtils;
import com.acheng.achengutils.utils.StringUtils;
import com.kymjs.rxvolley.RxVolley;
import com.kymjs.rxvolley.client.HttpCallback;
import com.kymjs.rxvolley.client.HttpParams;
import com.kymjs.rxvolley.http.VolleyError;
import acheng1314.cn.a3dbuild.MyApplication;
import acheng1314.cn.a3dbuild.bean.LoginBean;
import acheng1314.cn.a3dbuild.hostApi.MyApi;
import acheng1314.cn.a3dbuild.view.activity.viewcontroller.LoginActivityViewController;
/**
* Created by pc859107393 on 2016/9/12 0012.
*/
public class LoginActivityPresenter extends BasePresenter<LoginActivityViewController> {
/**
* 在子類的構(gòu)造函數(shù)中惹悄,設(shè)定參數(shù)為model春叫,這時(shí)候可以presenter調(diào)用接口來實(shí)現(xiàn)對(duì)界面的操作。
*
* @param model
*/
public LoginActivityPresenter(LoginActivityViewController model) {
super(model);
}
@Override
public void initData() {
}
public void doLogin(String name, String pwd) {
//用戶名和密碼不能為空
if (StringUtils.isEmpty(name) || StringUtils.isEmpty(pwd)) {
model.showDailog("用戶名或密碼不能為空!"); //調(diào)用model的錯(cuò)誤提示對(duì)話框
return;
}
//密碼MD5加密
pwd = CipherUtils.small32md5(pwd);
HttpParams params = new HttpParams();
params.put("userName", name);
params.put("passWord", pwd);
RxVolley.post(MyApi.LoginApi, params, new HttpCallback() {
@Override
public void onSuccess(String t) {
super.onSuccess(t);
//數(shù)據(jù)不為空再進(jìn)行數(shù)據(jù)處理
try {
if (null != t) {
MyApplication.getInstance().outLog("輸出", t);
LoginBean bean = new GsonUtils().toBean(t, LoginBean.class);
if (null != bean) {
if (bean.getCode() == 0) {
//請(qǐng)求成功
model.openHome(bean);
} else if (bean.getCode() == 1) {
model.showDailog("登錄失敗,帳戶不存在");
} else if (bean.getCode() == 2) {
model.showDailog("登錄失敗,密碼錯(cuò)誤");
} else {
model.showDailog("登錄失敗,其他未知錯(cuò)誤");
}
}
}
} catch (Exception e) {
e.printStackTrace();
model.showDailog("登錄失敗,其他未知錯(cuò)誤");
}
}
@Override
public void onFailure(VolleyError error) {
super.onFailure(error);
model.showDailog("登錄失敗,其他未知錯(cuò)誤");
}
@Override
public void onFinish() {
super.onFinish();
model.disProgressD(); //model的關(guān)閉對(duì)話框的接口
}
@Override
public void onPreStart() {
super.onPreStart();
model.showProgressD(); //model的進(jìn)度對(duì)話框
}
});
}
}
我們上面可以看到我們現(xiàn)在只要把請(qǐng)求網(wǎng)絡(luò)的數(shù)據(jù)傳遞上去就可以完成單元測(cè)試了暂殖,這樣子我們就達(dá)到了我們數(shù)據(jù)流轉(zhuǎn)的單元測(cè)試的標(biāo)準(zhǔn)价匠。
既然我們都看到了Presenter對(duì)model的調(diào)用,那么我們直接貼上model再對(duì)比Activity就能明白了我們是怎么完成這個(gè)設(shè)計(jì)的央星。
public interface LoginActivityViewController extends BaseViewController {
/**
* 顯示信息提示對(duì)話框
* @param msg message
*/
void showDailog(String msg);
/**
* 顯示進(jìn)度對(duì)話框
*/
void showProgressD();
/**
* 關(guān)閉對(duì)話框
*/
void disProgressD();
/**
* 登陸成功跳轉(zhuǎn)到其他界面
* @param bean
*/
void openHome(LoginBean bean);
}
我們看到這里,很多哥們可能又會(huì)不明白惫东,為什么我們能控制界面呢莉给?如下:
//我們?cè)诔绦蛑校琾resenter直接調(diào)用的model廉沮,但是model是被View實(shí)現(xiàn)了的颓遏。
public class LoginActivity extends BaseActivity<LoginActivityPresenter, LoginActivityViewController> implements LoginActivityViewController {
@Override
public void showDailog(String msg) {
//實(shí)現(xiàn)了model的顯示對(duì)話框的方法
new MustDoThingDailog("提示", msg, getContext(), new MustDoThingDailog.NeedDoThing() {
@Override
public void mustDoThings() {
}
});
}
@Override
public void showProgressD() {
//這是顯示進(jìn)度對(duì)話框的,實(shí)現(xiàn)了model的方法
}
@Override
public void disProgressD() {
//這是實(shí)現(xiàn)了moel的關(guān)閉進(jìn)度對(duì)話框的方法
}
@Override
public void openHome(LoginBean bean) {
//實(shí)現(xiàn)了model的打開其他頁面的方法
}
}
所以我們的MVP執(zhí)行的步驟其實(shí)就是:用戶執(zhí)行操作 -> 調(diào)用presenter(完成獨(dú)立的數(shù)據(jù)處理) -> 調(diào)用model的方法控制界面 -> 展示給用戶
然后應(yīng)該又有哥們會(huì)問我滞时,為什么你的基類中會(huì)有<>這種括號(hào)括起來的東西叁幢,恩恩這個(gè)是泛型,主要是用來說明他們是哪一類的東西坪稽,通過泛型來解耦就可以在基類中整合更多的東西曼玩。具體的要我來說明的話,我只能說“就不V习佟J蚺小!”篙梢,我需要任性一回顷帖。關(guān)于MVP更好的介紹可以看下github的項(xiàng)目TheMvp,這個(gè)是我的偶像@張濤寫的喲渤滞。
總結(jié)
- 在mvp架構(gòu)中,我們需要在基類中拿到每個(gè)界面對(duì)應(yīng)的presenter和model,則我們需要讓程序知道每個(gè)對(duì)應(yīng)的presenter和model.
- 為了減少不必要的代碼開銷,我們需要把每個(gè)activity和Fragment的公共方法抽取出來,寫入基類中.
- 在基類中,我們需要將具體的presenter和model解耦,則需要泛型進(jìn)行類型轉(zhuǎn)換來解除耦合.
- 泛型解除耦合后,我們需要在每個(gè)具體的view中來持有presenter和實(shí)現(xiàn)model層的接口.并且通過每個(gè)view關(guān)聯(lián)的presenter調(diào)用model的某個(gè)方法來控制view.