本篇文章是针对上一篇文章:带你封装本身的MVP+Retrofit+RxJava2框架(一)的进一步封装改进,建议在看完上一篇文章后,再食用本文效果更佳!java
文本已经收录到个人Github我的博客,欢迎大佬们光临寒舍:个人GIthub博客react
如上一篇文章所说,在MVP模式日渐流行的时候,封装一套MVP框架,不只对平常的开发大大便利,还能提早积累一下将来在实际工做中的技巧,而且,良好的封装和规范使用还能减小开发中的各类使人头疼的BUG。android
有人可能会问:“你上一篇不是也写了MVP框架吗?你这篇难道仍是同样的吗?难道你是换汤不换药吗?”git
其实,一开始笔者也觉得我上一篇文章封装的MVP框架已经够不错了,可是,在笔者某天看了yechaoa大神玩安卓java的源码后,被其封装的MVP框架的所折服,所以第一时间写这篇文章,想向你们分享下,笔者从中吸收的经验,但愿可以帮助到各位!github
本文相对上一篇文章的改进地方有下面几点:json
本项目基于Android X 进行构建,完整代码可在个人github上下载:带你封装本身的MVP+Retrofit+RxJava2框架(二)api
首先,能够看一下笔者项目的基本结构服务器
为了给你们模拟带自动获取Cookie的功能,因此笔者设计了一个具备登录,注册,收藏功能的Democookie
笔者在Demo中用到的框架以下
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.google.android.material:material:1.1.0'
//cardView
implementation 'androidx.cardview:cardview:1.0.0'
/*retrofit、rxjava*/
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
/*glide*/
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
/*butterknife*/
implementation 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
/*YUtils*/
implementation 'com.github.yechaoa:YUtils:2.1.0'
/*BRVAH*/
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.50'
/*banner*/
implementation 'com.youth.banner:banner:1.4.10'
复制代码
下面笔者将为你们详细介绍每一个类的相关信息
BaseActivity相对于笔者上一个版本的MVP框架,改进的地方是:将两个基类Activity合并为一个BaseActivity,而且在其中封装了进度条的显示和隐藏的方法
/** * Description : BaseActivity * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity implements BaseView {
protected P presenter;
protected abstract P createPresenter();
protected abstract int getLayoutId();
protected abstract void initView();
protected abstract void initData();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(LayoutInflater.from(this).inflate(getLayoutId(), null));
ButterKnife.bind(this);
presenter = createPresenter();
initView();
initData();
}
@Override
protected void onResume() {
super.onResume();
initListener();
}
@Override
protected void onDestroy() {
super.onDestroy();
//销毁时,解除绑定
if (presenter != null) {
presenter.detachView();
}
}
protected void initListener() {
}
@Override
public void showLoading() {
YUtils.showLoading(this, "加载中");
}
@Override
public void hideLoading() {
YUtils.dismissLoading();
}
/** * 能够处理异常 */
@Override
public void onErrorCode(BaseBean bean) {
}
/** * 启动activity * * @param activity 当前活动 * @param isFinish 是否结束当前活动 */
public void startActivity(Class<?> activity, boolean isFinish) {
Intent intent = new Intent(this, activity);
startActivity(intent);
if (isFinish) {
finish();
}
}
}
复制代码
/** * Description : BaseFragment * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseFragment<P extends BasePresenter> extends Fragment implements BaseView {
private Unbinder unbinder;
protected Context mContext;
protected P presenter;
protected abstract P createPresenter();
protected abstract int getLayoutId();
protected abstract void initView();
protected abstract void initData();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutId(), container, false);
unbinder = ButterKnife.bind(this, view);
//获得context,在后面的子类Fragment中均可以直接调用
mContext = ActivityUtil.getCurrentActivity();
presenter = createPresenter();
initView();
initData();
return view;
}
@Override
public void onResume() {
super.onResume();
initListener();
}
@Override
public void onDestroyView() {
super.onDestroyView();
//do something
unbinder.unbind();
//销毁时,解除绑定
if (presenter != null) {
presenter.detachView();
}
}
private void initListener() {
}
@Override
public void onErrorCode(BaseBean bean) {
}
/** * 显示加载中 */
@Override
public void showLoading() {
YUtils.showLoading(ActivityUtil.getCurrentActivity(), "加载中");
}
/** * 隐藏加载中 */
@Override
public void hideLoading() {
YUtils.dismissLoading();
}
}
复制代码
BasePresenter相对于笔者上一个版本的MVP框架,改进的地方是:将线程的调度写入了addDisposable中,并改写了addDisposable方法,使得调用方式更加简单优美
/** * Description : BasePresenter * * @author XuCanyou666 * @date 2020/2/7 */
public class BasePresenter<V extends BaseView> {
private CompositeDisposable compositeDisposable;
public V baseView;
/** * 这个后面能够直接用 Example:apiServer.login(username, password); */
protected API.WAZApi apiServer = RetrofitService.getInstance().getApiService();
public BasePresenter(V baseView) {
this.baseView = baseView;
}
/** * 解除绑定 */
public void detachView() {
baseView = null;
removeDisposable();
}
/** * 返回 view */
public V getBaseView() {
return baseView;
}
public void addDisposable(Observable<?> observable, BaseObserver observer) {
if (compositeDisposable == null) {
compositeDisposable = new CompositeDisposable();
}
compositeDisposable
.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer));
}
private void removeDisposable() {
if (compositeDisposable != null) {
compositeDisposable.dispose();
}
}
}
复制代码
Observer的基类,提供了自动显示和自动隐藏进度条的方法,对内处理了onStart,onError,onComplete方法,对外只提供了onSuccess和onError方法,符合使用习惯
/** * Description : BaseObserver * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseObserver<T> extends DisposableObserver<T> {
protected BaseView view;
private boolean isShowDialog;
public BaseObserver(BaseView view) {
this.view = view;
}
/** * 带进度条的初始化方法 * * @param view view * @param isShowDialog 是否显示进度条 */
public BaseObserver(BaseView view, boolean isShowDialog) {
this.view = view;
this.isShowDialog = isShowDialog;
}
@Override
protected void onStart() {
if (view != null && isShowDialog) {
view.showLoading();
}
}
@Override
public void onNext(T o) {
onSuccess(o);
}
@Override
public void onError(Throwable e) {
if (view != null && isShowDialog) {
view.hideLoading();
}
BaseException be;
if (e != null) {
//自定义异常
if (e instanceof BaseException) {
be = (BaseException) e;
//回调到view层 处理 或者根据项目状况处理
if (view != null) {
// 处理登陆失效 更新
view.onErrorCode(new BaseBean(be.getErrorCode(), be.getErrorMsg()));
} else {
onError(be.getErrorMsg());
}
//系统异常
} else {
if (e instanceof HttpException) {
//HTTP错误
be = new BaseException(BaseException.BAD_NETWORK_MSG, e);
} else if (e instanceof ConnectException || e instanceof UnknownHostException) {
//链接错误
be = new BaseException(BaseException.CONNECT_ERROR_MSG, e);
} else if (e instanceof InterruptedIOException) {
//链接超时
be = new BaseException(BaseException.CONNECT_TIMEOUT_MSG, e);
} else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
//解析错误
be = new BaseException(BaseException.PARSE_ERROR_MSG, e);
} else {
be = new BaseException(BaseException.OTHER_MSG, e);
}
}
} else {
be = new BaseException(BaseException.OTHER_MSG);
}
onError(be.getErrorMsg());
}
@Override
public void onComplete() {
if (view != null && isShowDialog) {
view.hideLoading();
}
}
public abstract void onSuccess(T o);
public abstract void onError(String msg);
}
复制代码
异常的基类
/** * Description : BaseException * * @author XuCanyou666 * @date 2020/2/7 */
public class BaseException extends IOException {
/** * 解析数据失败 */
public static final String PARSE_ERROR_MSG = "解析数据失败";
/** * 网络问题 */
public static final String BAD_NETWORK_MSG = "网络问题";
/** * 链接错误 */
public static final String CONNECT_ERROR_MSG = "链接错误";
/** * 链接超时 */
public static final String CONNECT_TIMEOUT_MSG = "链接超时";
/** * 未知错误 */
public static final String OTHER_MSG = "未知错误";
private String errorMsg;
private int errorCode;
public String getErrorMsg() {
return errorMsg;
}
public int getErrorCode() {
return errorCode;
}
public BaseException(String message) {
this.errorMsg = message;
}
public BaseException(String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorMsg = errorMsg;
}
public BaseException(int errorCode, String message) {
this.errorMsg = message;
this.errorCode = errorCode;
}
}
复制代码
实体类的基类,方便处理返回的json数据,具体的写法根据每一个API而定
/** * Description : BaseBean 实体类的基类 * * @author XuCanyou666 * @date 2020/2/7 */
public class BaseBean<T> implements Serializable {
/** * data : * errorCode : 0 * errorMsg : */
public T data;
public int errorCode;
public String errorMsg;
public BaseBean(int code, String data) {
this.errorCode = code;
this.data = (T) data;
}
}
复制代码
/** * Description : BaseView * * @author XuCanyou666 * @date 2020/2/7 */
public interface BaseView {
void showLoading();
void hideLoading();
void onErrorCode(BaseBean bean);
}
复制代码
持久化cookie,由于代码太多,这里只展现一个类的代码,详细代码请前往个人Github查看
package com.users.xucanyou666.rxjava2_retrofit_mvp2.http.cookie;
import android.content.Context;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
/** * Created by yechao on 2019/11/19/019. * Describe : */
public class CookiesManager implements CookieJar {
private final PersistentCookieStore cookieStore;
public CookiesManager(Context context) {
cookieStore = new PersistentCookieStore(context);
}
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookies.size() > 0) {
for (Cookie item : cookies) {
cookieStore.add(url, item);
}
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
return cookieStore.get(url);
}
}
复制代码
重写ResponseBodyConverter对json预处理,这里只展现一个类的代码,详细代码请前往个人Github查看
/** * Created by yechao on 2019/11/18/018. * Describe : 重写ResponseBodyConverter对json预处理 */
public class BaseResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
BaseResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
String jsonString = value.string();
try {
JSONObject object = new JSONObject(jsonString);
int code = object.getInt("errorCode");
if (0 != code) {
String data;
//错误信息
if (code == -1001) {//失效
data = "登陆失效,请从新登陆";
} else {
data = object.getString("errorMsg");
}
//异常处理
throw new BaseException(code, data);
}
//正确返回整个json
return adapter.fromJson(jsonString);
} catch (JSONException e) {
e.printStackTrace();
//数据解析异常即json格式有变更
throw new BaseException(BaseException.PARSE_ERROR_MSG);
} finally {
value.close();
}
}
}
复制代码
随着项目日渐庞大,请求也愈来愈多,不可能每一个请求都使用一个接口,这样不只形成浪费,更是不方便管理,所以,新建一个API做为Retrofit的管理类,用一个接口管理全部网络请求,能够有效改善代码质量
/** * Description : API * 接口的管理类 * * @author XuCanyou666 * @date 2020/2/7 */
public class API {
static final String BASE_URL = "https://www.wanandroid.com/";
public interface WAZApi {
//-----------------------【首页相关】----------------------
//首页文章列表 这里的{}是填入页数
@GET("article/list/{page}/json")
Observable<BaseBean<Article>> getArticleList(@Path("page") Integer page);
//-----------------------【登陆注册】----------------------
//登陆
@FormUrlEncoded
@POST("user/login")
Observable<BaseBean<User>> login(@Field("username") String username, @Field("password") String password);
//注册
@FormUrlEncoded
@POST("user/register")
Observable<BaseBean<User>> register(@Field("username") String username, @Field("password") String password, @Field("repassword") String repassword);
//-----------------------【 收藏 】----------------------
//收藏站内文章
@POST("lg/collect/{id}/json")
Observable<BaseBean> collectIn(@Path("id") Integer id);
//取消收藏---文章列表
@POST("lg/uncollect_originId/{id}/json")
Observable<BaseBean> uncollect(@Path("id") Integer id);
}
}
复制代码
Retrofit的配置类,在里面初始化了apiServer对象,并配置了日志信息,超时时间,Cookie持久化,用了静态内部类的单例模式
* Description : RetrofitService
*
* @author XuCanyou666
* @date 2020/2/8
*/
public class RetrofitService {
private static RetrofitService apiRetrofit;
private API.WAZApi apiServer;
//单例调用
public static RetrofitService getInstance() {
if (apiRetrofit == null) {
synchronized (Object.class) {
if (apiRetrofit == null) {
apiRetrofit = new RetrofitService();
}
}
}
return apiRetrofit;
}
//获取api对象
public API.WAZApi getApiService() {
return apiServer;
}
//初始化retrofit
private RetrofitService() {
//配置okhttp并设置时间、日志信息和cookies
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
//设置超时时间
.connectTimeout(15, TimeUnit.SECONDS)
//设置Cookie持久化
.cookieJar(new CookiesManager(YUtils.getApplication()))
.build();
//关联okhttp并加上rxjava和gson的配置和baseurl
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(BaseConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(API.BASE_URL)
.build();
apiServer = retrofit.create(API.WAZApi.class);
}
}
复制代码
这里的带有嵌套的实体类看似很复杂,其实能够经过一个在线JSON字符串转Java实体类的工具进行生成,须要注意的是,不要将BaseBean的那一层导入到实体类中
文章内容的实体类
/** * Description : Article * * @author XuCanyou666 * @date 2020/2/8 */
public class Article {
/** * curPage : 2 * datas : [{"apkLink":"","author":"叶应是叶","chapterId":67,"chapterName":"网络基......."}] * offset : 20 * over : false * pageCount : 62 * size : 20 * total : 1224 */
public int curPage;
public int offset;
public boolean over;
public int pageCount;
public int size;
public int total;
public List<DataDetailBean> datas;
public static class DataDetailBean {
/** * apkLink : * author : 叶应是叶 * chapterId : 67 * chapterName : 网络基础 * collect : false * courseId : 13 * desc : * envelopePic : * fresh : false * id : 2809 * link : https://www.jianshu.com/p/6d2f324c8f42 * niceDate : 2018-04-12 * origin : * projectLink : * publishTime : 1523532264000 * superChapterId : 98 * superChapterName : 网络访问 * tags : [] * title : 在 Android 设备上搭建 Web 服务器 * type : 0 * visible : 1 * zan : 0 */
public String apkLink;
public String author;
public int chapterId;
public String chapterName;
public boolean collect;
public int courseId;
public String desc;
public String envelopePic;
public boolean fresh;
public int id;
public int originId;
public String link;
public String niceDate;
public String origin;
public String projectLink;
public long publishTime;
public int superChapterId;
public String superChapterName;
public String title;
public int type;
public int visible;
public int zan;
public List<?> tags;
}
}
复制代码
/** * GitHub : https://github.com/yechaoa * CSDN : http://blog.csdn.net/yechaoa * <p> * Created by yechao on 2018/5/2. * Describe : */
public class User {
/** * collectIds : [] * email : * icon : * id : 3 * password : 111111 * type : 0 * username : 111111 */
public String email;
public String icon;
public int id;
public String password;
public int type;
public String username;
public List<?> collectIds;
public String repassword;
}
复制代码
这里分为每一个模块进行管理,本Demo有Login,Register,Home总共3个模块,在这里仅说明一个模块,其余的模块的写法也是相似的,具体其余模块的写法,能够上Github查看
LoginView层的接口
/** * Description : ILoginView * * @author XuCanyou666 * @date 2020/2/8 */
public interface ILoginView extends BaseView {
/** * 显示登录成功 * * @param successMessage 成功信息 */
void showLoginSuccess(String successMessage);
/** * 显示登录失败 * * @param errorMessage 失败信息 */
void showLoginFailed(String errorMessage);
void doSuccess(BaseBean<User> user);
}
复制代码
这里由于RxJava通过封装后,model层的代码太少,因此将Model直接写入Presenter中
/** * Description : LoginPresenter * * @author XuCanyou666 * @date 2020/2/8 */
class LoginPresenter extends BasePresenter<ILoginView> {
LoginPresenter(ILoginView baseView) {
super(baseView);
}
void login(String username, String password) {
addDisposable(apiServer.login(username, password), new BaseObserver<BaseBean<User>>(baseView, true) {
@Override
public void onSuccess(BaseBean<User> bean) {
baseView.showLoginSuccess("登陆成功( ̄▽ ̄)");
baseView.doSuccess(bean);
}
@Override
public void onError(String msg) {
baseView.showLoginFailed(msg + "(°∀°)ノ");
}
});
}
}
复制代码
登录界面输入框的监听器
/** * TextInputLayout监听器 * created by xucanyou666 * on 2020/2/7 18:09 * email:913710642@qq.com */
public class LoginTextWatcher implements android.text.TextWatcher {
private TextInputLayout mTilUsername;
private TextInputLayout mTilPassword;
LoginTextWatcher(TextInputLayout username, TextInputLayout password) {
mTilUsername = username;
mTilPassword = password;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
checkInput(mTilUsername);
checkInput(mTilPassword);
}
/** * 判断输入内容是否合法 * * @param textInputLayout textInputLayout */
public static void checkInput(TextInputLayout textInputLayout) {
if (textInputLayout != null) {
if (textInputLayout.getEditText().getText().length() > textInputLayout.getCounterMaxLength()) {
textInputLayout.setError("输入内容超过上限");
} else if (textInputLayout.getEditText().getText().length() < textInputLayout.getCounterMaxLength() / 2) {
textInputLayout.setError("最少6位");
} else {
textInputLayout.setError(null);
}
}
}
}
复制代码
/** * Description : LoginActivity * * @author XuCanyou666 * @date 2020/2/8 */
public class LoginActivity extends BaseActivity<LoginPresenter> implements ILoginView {
@BindView(R.id.et_username)
EditText mEtUsername;
@BindView(R.id.til_username)
TextInputLayout mTilUsername;
@BindView(R.id.et_password)
EditText mEtPassword;
@BindView(R.id.til_password)
TextInputLayout mTilPassword;
@BindView(R.id.btn_login)
Button mBtnLogin;
@BindView(R.id.btn_register)
Button mBtnRegister;
private String mUsername;
private String mPassword;
@Override
protected LoginPresenter createPresenter() {
return new LoginPresenter(this);
}
@Override
protected int getLayoutId() {
return R.layout.activity_login;
}
@Override
protected void initData() {
}
@Override
protected void initView() {
LoginTextWatcher textWatcher = new LoginTextWatcher(mTilUsername, mTilPassword);
mEtUsername.addTextChangedListener(textWatcher);
mEtPassword.addTextChangedListener(textWatcher);
}
@Override
public void showLoginSuccess(String successMessage) {
ToastUtil.showToast(successMessage);
}
@Override
public void showLoginFailed(String errorMessage) {
ToastUtil.showToast(errorMessage);
}
@Override
public void doSuccess(BaseBean<User> user) {
//存进sp里面
SpUtil.setBoolean(GlobalConstant.IS_LOGIN, true);
SpUtil.setString(GlobalConstant.USERNAME, user.data.username);
SpUtil.setString(GlobalConstant.PASSWORD, user.data.password);
startActivity(MainActivity.class, true);
}
/** * 判断帐号和密码输入是否正确 * * @return */
private boolean isValid() {
mUsername = mEtUsername.getText().toString().trim();
mPassword = mEtPassword.getText().toString().trim();
return check(mUsername, mTilUsername) && check(mPassword, mTilPassword);
}
/** * 判断输入是否正确 * * @param string 输入的内容 * @param textInputLayout textInputLayout控件 * @return */
private boolean check(String string, TextInputLayout textInputLayout) {
return !TextUtils.isEmpty(string) && string.length() <= textInputLayout.getCounterMaxLength() && textInputLayout.getCounterMaxLength() / 2 <= string.length();
}
@OnClick({R.id.btn_login, R.id.btn_register})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btn_login:
YUtils.closeSoftKeyboard();
if (isValid()) {
presenter.login(mUsername, mPassword);
} else {
ToastUtil.showToast("填写错误 (°∀°)ノ");
}
break;
case R.id.btn_register:
YUtils.closeSoftKeyboard();
startActivity(RegisterActivity.class, false);
break;
default:
break;
}
}
}
复制代码
有一天, 当我点开个人colors.xml资源文件的时候,发现是下图这个样子
而后当鼠标的光标移动到红色标记处,发现
The color “colorPrimary” in values has no declaration in the base values folder; this can lead to crashes when the resource is queried in a configuration that does not match this qualifier less…
接着我翻译了一下:
值中的颜色“colorPrimary”在基本值folde中没有声明
懵逼了,我不是声明了吗....最后仍是百度到告终果
解决方式是:先把colors文件剪切下来,再粘回去。
感受是AS的BUG....我用的AS版本是3.5.1
固然百度了一下,解决方式是:在根目录的build.gradle中添加maven
就是下面这样
发生问题的场景,就是我在presenter中设置了请求文章列表的数据的时候,会自动显示和隐藏进度条,可是请求完文章列表后,不能自动隐藏
通过浏览代码,发现,个人请求文章列表的方法写多了一次,解决方法:只保存onResume里面的一次
若是文章对您有一点帮助的话,但愿您能点一下赞,您的点赞,是我前进的动力
本文参考连接: