MVP模式在Android开发中的最佳实践
这篇文章拖了好久了,一直存在草稿箱里没有继续写,趁今天有空,撸撸完。
回想一下,你刚刚学习Android的时候,总会看到一些书上写着,Android使用的是MVC模式,Activity就是一个Controller,或许那个时候,你没有什么深刻的体会。随着经验的积累。你发现,Activity既是Controller,掌管着许许多多的业务逻辑,同时它也作为View的一部分,控制着视图层的显示。久而久之,这个Controller便显得过于重,职责不再那么单一。
于是,再后来,为了使Activity的职责更加单一,便出现了MVP,MVVM等模式,只能说各有各的优点,没有谁对谁错,一个模式有另一个模式不具有的特点,同时也不具备另一个模式具有的特点,架构的选择永远是根据业务的复杂程度来进行的。MVC有其特点,就是写代码简单啊,但是其缺点也很明显,业务复杂起来后,Activity显得过于庞大不是特别好维护。至于MVVM,个人是十分排斥这种模式的,为什么呢,在XML中写数据绑定的代码显得有点蛋疼,从而使得xml的职责不是那么单一,在我看来,xml用来作为View再好不过了,不必掺和其他任何元素进来,这样显得“不干净”。而MVP呢,我觉得在Android开发中,MVP是一个值得考虑的模式,它既没有MVVM那样,在xml中写数据绑定的代码,xml依然还是原来的配方,也没有MVC那样,拥有一个臃肿的Controller,取而代之的是更加清晰的分层,职责更加单一,当然,优点背后必然有缺点,相信用过MVP的都知道有什么缺点,那就是接口的定义会暴增。
那么什么是MVP模式呢?
M即Model,what to show? 也就是显示在UI上的数据,至于数据怎么来,数据库,网络等等渠道,都是属于这一层
V即View,how to show?也就是怎么显示数据,在Android中,通常是使用xml定义这个view,一般View中会持有Presenter的引用。
P即Presenter,Presenter扮演着中间联系人的作用,就好比MVC中的Controller,通常来说,Presenetr中一般会持有View和Model的引用。
这三者的联系如下图所示:
那么问题来了,该如何实现MVP模式呢?这里介绍一个开源库Mosby,github地址https://github.com/sockeqwe/mosby
本篇文章不对该库的具体实现作分析,如果对实现感兴趣的可以阅读源码,毕竟源码之前,了无秘密。在使用前,先加入对该库的依赖
dependencies {
compile 'com.hannesdorfmann.mosby:mvp:2.0.1'
compile 'com.hannesdorfmann.mosby:viewstate:2.0.1'
}
现在假设我们实现一个登陆功能,原来的MVC方式就是先定义好xml,然后直接在Activity中书写各种业务逻辑,导致Activity越来越庞大,而使用了MVP之后,Activity会显得十分干净。
XML的定义这里就不再贴了,两个输入框(账号和密码),一个登陆按钮。
首先,我们需要一个与服务器交互的接口,为了简单起见,我们在本地进行模拟,如果账号密码都是admin,则登陆成功,如果账号密码都是server,其他情况都返回账号或密码错误。理论上,这个需要在子线程中发起请求,再通过UI线程回调,这一步也省略,直接在主线程中判断并回调,由于是本地模拟,不会产生任何卡顿,实际使用时需严格按照子线程请求主线程回调。
public interface Listener {
void onSuccess(T t);
void onFailure(int code);
}
public class LoginApi {
public static void login(String username, String password, Listener listener) {
if (username.equals("admin") && password.equals("admin")) {
listener.onSuccess(null);
} else if (username.equals("server") && password.equals("server")) {
listener.onFailure(LoginView.SERVER_ERROR);
} else {
listener.onFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
}
}
}
业务逻辑的接口定义好了,这个LoginApi可以认为是Model层,接下来我们需要定义和Login相关的View,Presenter。
首先定义一个LoginView接口继承MvpView接口,由于登录的接口有两种情况,一种是登录成功,一种是登录失败,而登录失败的情况又有多种,于是需要通过一个状态码进行区分,于是LoginView中的接口就产生了。这里我们直接将各种错误状态定义在了LoginView中,实际使用时建议定义在一个常量类中进行统一管理。
public interface LoginView extends MvpView {
public static final int USERNAME_OR_PASSWORD_EMPTY = 0x01;
public static final int USERNAME_OR_PASSWORD_ERROR = 0x02;
public static final int SERVER_ERROR = 0x03;
void onLoginSuccess();
void onLoginFailure(int code);
}
然后定义一个LoginPresenter类继承MvpBasePresenter,泛型参数是LoginView,在里面调用LoginApi的接口并将接口返回。
public class LoginPresenter extends MvpBasePresenter {
public void login(final String username, final String password) {
if (username == null || username.equals("")) {
LoginView view = getView();
if (view != null) {
view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
return;
}
} else if (password == null || password.equals("")) {
LoginView view = getView();
if (view != null) {
view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
return;
}
}
Listener listener = new Listener() {
@Override
public void onSuccess(String str) {
LoginView view = getView();
if (view != null) {
view.onLoginSuccess();
}
}
@Override
public void onFailure(int code) {
if (code == LoginView.USERNAME_OR_PASSWORD_ERROR) {
LoginView view = getView();
if (view != null) {
view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
}
} else {
LoginView view = getView();
if (view != null) {
view.onLoginFailure(LoginView.SERVER_ERROR);
}
}
}
};
LoginApi.login(username, password, listener);
}
}
最后便是让Activity实现LoginView接口,实现LoginView中定义的接口,此外,还需要继承MvpActivity,泛型参数是LoginView和LoginPresenter,并实现抽象方法createPresenter()返回LoginPresenter,而在LoginView中定义的两个接口onLoginSuccess和onLoginFailure中,全都是UI相关的代码,整个Activity中不再有业务逻辑的代码,职责也就单一了。
public class LoginActivity extends MvpActivity implements View.OnClickListener, LoginView {
private EditText etAccount;
private EditText etPassword;
private Button btnLogin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etAccount = (EditText) findViewById(R.id.accout);
etPassword = (EditText) findViewById(R.id.password);
btnLogin = (Button) findViewById(R.id.login);
btnLogin.setOnClickListener(this);
}
@NonNull
@Override
public LoginPresenter createPresenter() {
return new LoginPresenter();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.login:
onLogin();
break;
}
}
private void onLogin() {
String username = etAccount.getText().toString();
String passowrd = etPassword.getText().toString();
getPresenter().login(username, passowrd);
}
@Override
public void onLoginSuccess() {
Toast.makeText(this, "登陆成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onLoginFailure(int code) {
switch (code) {
case LoginView.USERNAME_OR_PASSWORD_EMPTY:
Toast.makeText(this, "账号或密码不能为空", Toast.LENGTH_SHORT).show();
break;
case LoginView.USERNAME_OR_PASSWORD_ERROR:
Toast.ma