Android MVP是安卓开发中一个经典的话题,当项目较大、参与的开发人员较多,MVP的优势就体现出来了。
系列文章 Android组件化-基础框架搭建 Android组件化-组件间通信BRouter Android组件化-风格统一&主题变色 Android组件化-MVP设计模式
经典的意思,就是又老又香 ^-^
提到Android MVP(Model-View-Presenter)就会想到MVC(Model-View-Controller),C就是Web开发中经常提到的Controller,P则是Android中用来分离Activity逻辑与界面的Presenter。
MVP核心思想:
MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model。
一图胜千言:
Modulize项目使用MVP作为基本的开发框架(以登录为例)。
Model层负责数据交互,包括网络交互、本地数据库交互以及SharedPreferences数据存取。在lib-common中添加抽象类BaseModel,LoginModel等业务模块继承自BaseModel。
public abstract class BaseModel {
}
网络访问使用无话可说的okHttp,结合优雅的Retrofit,加以RxJava,真香!
使用okHttpClient实例管理全局http访问:
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接口访问实例,
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格式:
public class Response<T> {
private int code; private String message; private T data;
...}
基于Retrofit的登录Api如下:
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);}
使用J神家的的GreenDao,这个移动端ORM框架还是需要好好学习下的,本文仅介绍GrrenDao在MVP中的使用。在lib-db中创建DBHelper用于管理数据库连接和数据访问对象(Dao)实例:
public class DBHelper {
... instance init
public <T> AbstractDao getDao(Class<T> clazz) { return session.getDao(clazz); }}
使用SP存储用户偏好设置或登录认证数据等碎片数据。
Model中持有Retrofit实例(api)、数据库访问对象(Dao)以及SP等本地存储对象:
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中:
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); }}
Presenter持有Model实例,Presenter初始化时实例化Model,在lib-common中加入BasePresenter:
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:
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构造函数中初始化。
在lib-common中定义BaseView,
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();
}
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(); } }}
类似BaseActivity,加入一些对宿主Activity的回调。
参考https://github.com/blackist/modulize/blob/8478eb2a4bdaf7b9f9e2022be0e9462ea82b3eeb/lib-common/src/main/java/org/blackist/common/base/BaseFragment.java
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
(完)