Android MVP 架构重构实践

我们之前的架构就是普通的MVC架构。由于各种layout文件职责单一,因此Activity经常也作为Controller层,这样可能会堆出一个几千行的Activity,看起来就很爽。。因此,这次总结项目时便计划用更好的MVP架构和MVVM架构来重构项目。

为了简便起见,就拿LoginActivity来练手吧,这还属于逻辑比较简单的。。这是原登录逻辑的大概代码(省略了大量逻辑):

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
public class LoginActivity extends BaseActivity {
@Bind(R.id.et_username)
EditText etUserName;
@Bind(R.id.et_password)
EditText etPassword;
private String mUserName = "";
private String mPassword = "";
@Override
protected int getLayoutId() {
return R.layout.activity_login;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected boolean hasBackButton() {
return true;
}
@Override
protected int getActionBarTitle() {
return R.string.login;
}
@Override
@OnClick({R.id.btn_login, R.id.iv_qq_login, R.id.iv_wx_login, R.id.iv_sina_login})
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.btn_login:
handleLogin();
break;
case R.id.iv_qq_login:
qqLogin();
break;
case R.id.iv_wx_login:
wxLogin();
break;
case R.id.iv_sina_login:
sinaLogin();
break;
default:
break;
}
}
private boolean okForLogin() {
if (!DeviceUtil.hasInternet()) {
ToastUtil.toast(R.string.tip_no_internet);
return false;
}
....
return true;
}
private final BaseJsonHttpResponseHandler mHandler = new BaseJsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, String rawJsonResponse, Object response) {
if(response != null) {
handleLoginResult((LoginResult)response);
}
}
@Override
public void onFailure(int statusCode, Header[] headers, Throwable throwable, String rawJsonData, Object errorResponse) {
ToastUtil.toast("网络出现错误,请重试。(" + statusCode + ")");
}
@Override
protected Object parseResponse(String rawJsonData, boolean isFailure) throws Throwable {
return JsonUtil.resolveLoginResult(rawJsonData);
}
@Override
public void onFinish() {
super.onFinish();
hideWaitDialog();
}
};
private void handleLogin() {
if(!okForLogin())
return;
mUserName = etUserName.getText().toString();
mPassword = etPassword.getText().toString();
hideKeyboard();
showWaitDialog(R.string.progress_login).show();
SamsaraAPI.login(mUserName, mPassword, mHandler);
}
private void handleLoginResult(LoginResult result) {
if(result.getDataResult().isOK()) {
...
AppContext.getContext().saveUserInfo(result.getUser());
hideWaitDialog();
handleLoginSuccess();
}
else {
AppContext.getContext().cleanLoginInfo();
ToastUtil.showToast("错误:" + result.getDataResult().getErrorMsg());
}
}
private void handleLoginSuccess() {
...
ToastUtil.toast("登录成功");
finish();
}
private void qqLogin() {
...
}
private void wxLogin() {
...
}
private void sinaLogin() {
...
}
}

如果登录逻辑很复杂(比如有很多种三方登录逻辑),那么Activity可能会比较臃肿。这时候就要减轻Activity的职责,把代码重构成MVP架构。这个时候Activity作为View层,而把主要业务逻辑放至Presenter层。Presenter层与View层的交互是通过接口实现的。
重构后的架构
IBaseLoginService接口为普通登录接口,IThirdLoginService接口继承普通登录接口,也扩展作为三方扩展登录接口,登录逻辑的具体实现在LoginServiceImpl类里。
我们把登录接口设计成这样:

1
2
3
4
5
public interface IBaseLoginService {
void login(String username, String password, LoginResultCallback callback);
}

1
2
3
4
5
6
7
8
public interface IThirdLoginService extends IBaseLoginService {
void wxLogin(String username, String password, LoginResultCallback callback);
void qqLogin(String username, String password, LoginResultCallback callback);
void weiboLogin(String username, String password, LoginResultCallback callback);
}

我们同时设计了登录的回调接口,用于处理登录成功或失败的逻辑:

1
2
3
4
5
6
public interface LoginResultCallback {
void onLoginSuccess(LoginResult result);
void onFailure(int error_code);
}

接下来我们在LoginServiceImpl类里实现登录逻辑:

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
public class LoginServiceImpl implements IThirdLoginService {
@Override
public void login(final String username, final String password, final LoginResultCallback callback) {
final BaseJsonHttpResponseHandler mHandler = new BaseJsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, String rawJsonResponse, Object response) {
if(response != null) {
callback.onLoginSuccess((LoginResult)response);
}
}
@Override
public void onFailure(int statusCode, Header[] headers, Throwable throwable, String rawJsonData, Object errorResponse) {
callback.onFailure(statusCode);
}
@Override
protected Object parseResponse(String rawJsonData, boolean isFailure) throws Throwable {
return JsonUtil.resolveLoginResult(rawJsonData);
}
};
new Thread() {
@Override
public void run() {
SamsaraAPI.login(username, password, mHandler);
}
}.start();
}
//TODO:此处省略第三方登录逻辑
@Override
public void wxLogin(String username, String password, LoginResultCallback callback) {
LogUtil.toast("微信API 接口未开放");
}
@Override
public void qqLogin(String username, String password, LoginResultCallback callback) {
LogUtil.toast("QQ API 接口未开放");
}
@Override
public void weiboLogin(String username, String password, LoginResultCallback callback) {
LogUtil.toast("微博API 接口未开放");
}
}

其中,这里对网络的处理用了AsyncHttpClient库,以后也可以替换成Volley;SamsaraAPI是为此App设计的API类。到这里,如果后边三个三方实现比较臃肿的话,整个登录业务逻辑类就显得比较臃肿了,我思考了一下,再把三方逻辑的实现单列一个类,需要的时候继承?不过用继承也不是很好的方法,可以考虑用组合或者装饰模式来实现(此处留待想到更好的设计模式)。

Addition:如果在登录之前需要对登录数据及环境进行处理,且处理逻辑比较多的话,我们可以借助AOP的思想,利用代理模式,将这些通用逻辑(横切面)与特定业务逻辑(纵面)分开,更好地解除耦合。如日志、数据校验之类的逻辑都属于通用逻辑;当然另一种方法就是封装成各种Util类,我这里采用了后一种方法。

MVC与MVP架构(图片来源于网络)

接下来就是View层接口了。前边提到过,Presenter与View是通过接口进行交互的,因此这个View接口如何设计也至关重要。我这里考虑的是,对于我们的一个业务逻辑:

  • 此逻辑需要获取什么数据?
  • 此逻辑进行时需要有什么表现?(比如进度条对话框)
  • 逻辑执行的结果以及对UI的反馈?

比如,对于登录逻辑,我们需要从UI中获取用户名和密码;登录请求发出后需要等待服务器响应,这段时间应该会显示等待对话框;登录响应后,返回登录成功或失败的信息,对应更新UI和数据,因此我设计成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IBasicLoginView {
String getUsername();
String getPassword();
void handleLoginSuccess();
void showWaiting();
void hideWaiting();
boolean okForLogin();
}

然后就是Presenter的设计了,Presenter与LoginService和LoginView使用组合模式:

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
public class LoginPresenter {
private IThirdLoginService loginService;
private IBasicLoginView loginView;
private Handler mHandler = new Handler();
public LoginPresenter(IBasicLoginView loginView) {
this.loginView = loginView;
this.loginService = new LoginServiceImpl();
}
public void login() {
if(!loginView.okForLogin())
return;
loginView.showWaiting();
loginService.login(loginView.getUsername(), loginView.getPassword(),
new LoginResultCallback() {
@Override
public void onLoginSuccess(final LoginResult result) {
mHandler.post(new Runnable() {
@Override
public void run() {
handleLoginResult(result);
loginView.hideWaiting();
loginView.handleLoginSuccess();
}
});
}
@Override
public void onFailure(int error_code) {
ToastUtil.toast("网络出现错误,请重试。(" + error_code + ")");
}
});
}
private void handleLoginResult(LoginResult result) {
...
}
public void qqLogin() {
loginService.qqLogin("", "", null);
}
public void wxLogin() {
loginService.wxLogin("", "", null);
}
public void sinaLogin() {
loginService.weiboLogin("", "", null);
}
}

最后实现Activity:

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
public class LoginActivity extends BaseActivity implements IBasicLoginView {
@Bind(R.id.et_username)
EditText etUserName;
@Bind(R.id.et_password)
EditText etPassword;
private LoginPresenter presenter = new LoginPresenter(this);
@Override
protected int getLayoutId() {
return R.layout.activity_login;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected boolean hasBackButton() {
return true;
}
@Override
protected int getActionBarTitle() {
return R.string.login;
}
@Override
@OnClick({R.id.btn_login, R.id.iv_qq_login, R.id.iv_wx_login, R.id.iv_sina_login})
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.btn_login:
presenter.login();
break;
case R.id.iv_qq_login:
presenter.qqLogin();
break;
case R.id.iv_wx_login:
presenter.wxLogin();
break;
case R.id.iv_sina_login:
presenter.sinaLogin();
break;
default:
break;
}
}
@Override
public boolean okForLogin() {
if (!DeviceUtil.hasInternet()) {
ToastUtil.toast(R.string.tip_no_internet);
return false;
}
...
return true;
}
@Override
public String getUsername() {
return etUserName.getText().toString();
}
@Override
public String getPassword() {
return etPassword.getText().toString();
}
@Override
public void showWaiting() {
hideKeyboard();
showWaitDialog(R.string.progress_login).show();
}
@Override
public void hideWaiting() {
hideWaitDialog();
}
@Override
public void handleLoginSuccess() {
...
ToastUtil.toast("登录成功");
finish();
}
}

由于这只是练手,原Activity逻辑也不算也别复杂(五百行左右),因此对架构进行重构后并没有看出开发上有什么便捷之处,但以后可能会有更为复杂的逻辑,若不进行适当的重构,Activity可能会超过1000行,特别臃肿,不利于扩展和调BUG。因此,使用MVP/MVVM架构,在一个大项目中是必不可少的。

文章目录