Android组件化-MVP设计模式

Anroid MVP是安卓开发中一个经典的话题,当项目较大、参与的开发人员较多,MVP的优势就体现出来了。

系列文章

Android组件化-基础框架搭建

Android组件化-组件间通信BRouter

Android组件化-风格统一&主题变色

Android组件化-MVP设计模式

一、经典的MVP

经典的意思,就是又老又香 ^-^

1.1 一句话MVP

提到Android MVP(Model-View-Presenter)就会想到MVC(Model-View-Controller),C就是Web开发中经常提到的Controller,P则是Android中用来分离Activity逻辑与界面的Presenter。

MVP核心思想:

1
2
MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model

1.2 MVP图解

一图胜千言:

MVP模型图

  • 视图View:Activity和Fragment
  • 逻辑Presenter:业务逻辑和业务管理类等
  • 模型Model:SharedPreferences、数据库访问(Dao)和网络交互(Api)

二、Modulize使用MVP

Modulize项目使用MVP作为基本的开发框架(以登录为例)。

2.1 Model层的设计

Model层负责数据交互,包括网络交互、本地数据库交互以及SharedPreferences数据存取。在lib-common中添加抽象类BaseModel,LoginModel等业务模块继承自BaseModel。

1
2
3
4
public abstract class BaseModel {

}

网络交互 - okHttp+Retrofit+Rxjava

网络访问使用无话可说的okHttp,结合优雅的Retrofit,加以RxJava,真香!

使用okHttpClient实例管理全局http访问:

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
public class OkHttp3Util {

private static OkHttpClient mOkHttpClient;

/**
* 获取OkHttpClient对象实例
*
* @return
*/
public static OkHttpClient getOkHttpClient() {

if (null == mOkHttpClient) {

// build design mode
mOkHttpClient = new OkHttpClient.Builder()
// cookie manager
.cookieJar(new CookiesManager())
// 网络请求日志
.addInterceptor(loggingInterceptor)
// 自定义拦截器
.addInterceptor(new CommonIntercepter())
// set timeout of connection, reading and writing
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.cache(cache)
.build();
}

return mOkHttpClient;
}
}

在lib-common中新建ServiceGenerate类管理、创建Retrofit接口访问实例,

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
public class ServiceGenerator {

private static final String API_SERVICE = "http://xxxx:8080/api/";

/**
* 在gson中加入时间格式化,DateDeserializer\DateSerializer为自定义转换类.
*/
private static Gson gson = new GsonBuilder()
.registerTypeAdapter(java.util.Date.class, new DateDeserializer()).setDateFormat(DateFormat.LONG)
.registerTypeAdapter(java.util.Date.class, new DateSerializer()).setDateFormat(DateFormat.LONG)
.create();

/**
* API Retrofit.
*/
private static Retrofit apiGenerator = new Retrofit.Builder()
.baseUrl(API_SERVICE)
// 自定义转换器一定要在gsonConverter前面,否则gson会拦截所有的解析方式
.addConverterFactory(CustomConverterFactory.create())
// Gson Converter
.addConverterFactory(GsonConverterFactory.create(gson))
// Callback Handler RxJava
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(OkHttp3Util.getOkHttpClient())
.build();
}

为了统一处理Http接口返回,创建Response响应类,应当和后台接口保持一致的gson格式:

1
2
3
4
5
6
7
8
public class Response<T> {

private int code;
private String message;
private T data;

...
}

基于Retrofit的登录Api如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface LoginApi {

/**
* user login
*
* @param username username
* @param password password
* @return user info
*/
@FormUrlEncoded
@POST("login")
Observable<Response<User>> loginStu(@Field("username") String username, @Field("password") String password);
}

数据库交互 - GreenDao

使用J神家的的GreenDao,这个移动端ORM框架还是需要好好学习下的,本文仅介绍GrrenDao在MVP中的使用。在lib-db中创建DBHelper用于管理数据库连接和数据访问对象(Dao)实例:

1
2
3
4
5
6
7
8
public class DBHelper {

... instance init

public <T> AbstractDao getDao(Class<T> clazz) {
return session.getDao(clazz);
}
}

SharedPreferences

使用SP存储用户偏好设置或登录认证数据等碎片数据。

LoginModel

Model中持有Retrofit实例(api)、数据库访问对象(Dao)以及SP等本地存储对象:

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
public class LoginModel extends BaseModel {

private static final String TAG = "LoginModel";

private LoginApi api;
private UserDao userDao;
private SharedPreferences userPreference;

public LoginModel() {
// 使用ServiceGenerator生成api访问类
api = ServiceGenerator.createAPIService(LoginApi.class);
// 获取数据库访问对象
userDao = (UserDao) DBHelper.getInstance().getDao(User.class);
userPreference = context.getSharedPreferences("user", Context.MODE_PRIVATE);
}

public void setUser(User user) {
userPreference.put("user", user.getName());
userDao.insert(user);
}

public void login(String username, String password, Observer<Response<User>> observer) {
rxSubscribe(api.login(username, password), observer);
}
}

Presenter调用LoginModel方法时传递接口参数和Observer,LoginModel接口请求响应后回调Observer,rxSubscribe()定义在BaseModel中:

1
2
3
4
5
6
7
8
public abstract class BaseModel {
protected static <T> void rxSubscribe(Observable<T> observable, Observer<T> observer) {
observable.subscribeOn(Schedulers.io())
.subscribeOn(Schedulers.newThread())//子线程访问网络
.observeOn(AndroidSchedulers.mainThread())//回调到主线程
.subscribe(observer);
}
}

2.2 Presenter层的设计

Presenter持有Model实例,Presenter初始化时实例化Model,在lib-common中加入BasePresenter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class BasePresenter<TView extends BaseView, TModel extends BaseModel> {

protected TView mView;
protected TModel mModel;

public BasePresenter(TView view) {
this.mView = view;
this.mModel = this.getModel();
}

protected abstract TModel getModel();

public void detach() {
this.mView = null;
}
}

LoginPresenter集成BasePresenter,实例化LoginModel:

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
public class LoginPresenter extends BasePresenter<BaseActivity, LoginModel> {

public LoginPresenter(BaseActivity activity) {
super(activity);
}

@Override
protected LoginModel getModel() {
return new LoginModel();
}

public void login(String username, String password) {
// 请求前 加载等待框
mView.loadHud();
mModel.loginStudent(username, password, new Observer<Response<User>>() {
@Override
public void onCompleted() {
}

@Override
public void onError(Throwable e) {
e.printStackTrace();
}

@Override
public void onNext(Response<User> response) {
// 加载完成 取消等待框
mView.cancelHud();

if (response.OK()) {
// 请求成功 回调VIew层进行页面刷新
mView.onViewEvent(BaseView.VIEW_LOADED, response.getData());
// 把用户信息保存在本地
mModel.setUser(user);
} else {
// 请求失败 回调View层报错
mView.onViewEvent(LoginActivity.ERROR, null);
}
}
});
}
}

本项目在MVP中未使用接口的方式,在View中实现接口,在Presenter中持有实例并进行接口调用,因为使用接口则每个页面都需要新建一个接口类,较为繁琐。

本项目MVP使用BaseView中的抽象方法onViewEvent(),每个View继承BaseView后实现onViewEvent(int code, Object param),Presenter层Attach BaseView后通过mView.onViewEvent()对View进行界面回调处理,View中根据事件code和参数param进行视图处理。

一个Presenter可持有多个Model,定义多个Model对象并在Presenter构造函数中初始化。

2.3 View层的设计

在lib-common中定义BaseView,

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
void toast(@StringRes int resId);

/**
* 用于Presenter中吐司提示
*/
void toast(String res);

<T extends View> T findViewById(@IdRes int resId);

/**
* 用于Presenter回调界面操作
*/
void onViewEvent(int code, Object param);

/**
* 在界面中统一处理数据、网络异常
*/
void onViewState(int state);
void onViewState(Response response);

/**
* 加载、取消Dialog
*/
void loadHud();
void cancelHud();

}
  • toast():Toast封装,用于在Activity、Fragment或Presenter中弹出用户提示
  • findViewById():主要用于fragment中获取元素使用(组件化开发使用ButterKnife较为繁琐,不建议使用)
  • onViewEvent():View层的回调,用于Presenter网络请求响应后通知View层
  • onViewState():View层的回调。当Presenter层发生错误时统一处理View(网络异常、Http请求错误等)
  • loadHud()/cancelHud():加载ProgressDialog,Presenter发请网络请求时、请求结束后,在Presenter层弹出ProgressDialog

BaseActivity

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
public abstract class BaseActivity<TPresenter extends BasePresenter> extends AppCompatActivity implements BaseView {

protected Handler mUIHandler;

protected TPresenter mPresenter;

protected KProgressHUD mHud;

// 获取界面layout资源文件
@LayoutRes
protected abstract int getLayoutResId();

protected abstract void initViewAndData(@Nullable Bundle savedInstanceState);

protected abstract TPresenter getPresenter();

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
beforeCreate();
super.onCreate(savedInstanceState);
beforeSetContentView();
setContentView(this.getLayoutResId());
// init
this.init();
this.initViewAndData(savedInstanceState);

// EventBus
EventBus.getDefault().register(this);
}

/**
* before set contentView
*/
private void beforeSetContentView() {
// NoTitle
requestWindowFeature(Window.FEATURE_NO_TITLE);
// ScreenPortrait
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}

/**
* before create
*/
private void beforeCreate() {
// 统一设置主题
setTheme(UIConfig.getInstance(getApplicationContext()).getThemeId());
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(CommonEvent event) {
// EventBus统一处理全局异常
BLog.e("[Event]: " + event.code);
if (event.code == CommonEvent.Type.NETWORK_ERROR) {
onViewState(UIConstants.ViewState.NETWORK_DISCONNECTED);
if (this.mCommonEvent != null) {
this.mCommonEvent.onCommonEvent(event.code, event.param);
}
// cancel loading hud
cancelHud();
}
}

/**
* init view, e.g commonTitleBar.
*/
private void init() {
// view init
...

// 从子类拿到Presenter实例
this.mPresenter = this.getPresenter();
// 使用第三方库作为Loading Dialog
this.mHud = KProgressHUD.create(this);
}

@Override
public void onViewState(int state) {
// 全局异常处理
...
}

@Override
public void onViewState(Response response) {
// 根据Response处理服务器http响应
...
}

@Override
protected void onDestroy() {
super.onDestroy();

this.mUIHandler = null;
// Unregister EventBus
EventBus.getDefault().unregister(this);
}

@Override
public <T extends View> T findViewById(int resId) {
return super.findViewById(resId);
}

@Override
public void toast(@StringRes final int resId) {
if (mUIHandler == null) {
return;
}
mUIHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(
getApplicationContext(),
resId,
Toast.LENGTH_SHORT).show();
}
});
}

@Override
public void toast(final String res) {
...
}

@Override
public void loadHud(int resId) {
// 加载等待Dialog
if (mHud == null) {
mHud = KProgressHUD.create(this);
}
mHud.setStyle(KProgressHUD.Style.SPIN_INDETERMINATE)
.setCancellable(true)
.setLabel(resId == 0 ? getString(R.string.opt_loading) : getString(resId))
.setAnimationSpeed(1)
.setDimAmount(0.5f)
.show();
}

@Override
public void cancelHud() {
if (mHud != null) {
mHud.dismiss();
}
}
}

BaseFragment

类似BaseActivity,加入一些对宿主Activity的回调。

参考https://github.com/blackist/modulize/blob/8478eb2a4bdaf7b9f9e2022be0e9462ea82b3eeb/lib-common/src/main/java/org/blackist/common/base/BaseFragment.java

LoginActivity

LoginActivity继承自BaseActivity,实例化LoginPresenter,实现onViewEvent()回调函数:

public class LoginActivity extends BaseActivity<LoginPresenter> implements View.OnClickListener {

    private static final String TAG = "LoginActivity";

    public static final int ERROR = 1000;

    @Override
    protected int getLayoutResId() {
        return R.layout.main_login_activity;
    }

    @Override
    protected void initViewAndData(@Nullable Bundle savedInstanceState) {
        initView();
        ...
    }

    @Override
    protected LoginPresenter getPresenter() {
        return new LoginPresenter(this);
    }

    @Override
    public void onClick(View v) {
        ...
    }

    @Override
    public void onViewEvent(int code, Object param) {
        switch (code) {
            case VIEW_LOADED: {
                // 登录成功处理
                ...
                startActivity(new Intent(this, MainActivity.class));
                finish();
            }
            break;

            case ERROR: {
                toast(R.string.main_login_error);
            }
            break;

            default:
        }
    }
}

通常情况下一个View对应一个Presenter,也可在View中定义多个Presenter对象并在initViewAndData()中初始化

至此,实现了精简版的Android MVP,本人用在项目开发中问题不大。

参考

https://segmentfault.com/a/1190000003927200

https://juejin.im/post/5a61559051882573351a5fb6

(完)


Android组件化-MVP设计模式
https://blackist.org/2019-03-31-android-modulize-mvp/
作者
董猿外
发布于
2019年3月31日
许可协议