带你封装自己的MVP+Retrofit+RxJava2框架(二)

前言

本篇文章是针对上一篇文章:带你封装自己的MVP+Retrofit+RxJava2框架(一)的进一步封装改进,建议在看完上一篇文章后,再食用本文效果更佳!

本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:我的GIthub博客

本篇文章需要已经具备的知识:

  • MVP的概念和基本使用
  • Retrofit框架的基本使用
  • RxJava2框架的基本使用
  • ButterKnife框架的基本使用
  • Base基类的概念
  • YUtils的简单使用
  • BaseRecyclerViewAdapterHelper的简单使用

学习清单:

  • Base实体类的封装
  • Base异常类的封装
  • Base观察者的封装
  • RxJava线程自动调度的小技巧
  • 进行网络请求自动显示加载中
  • 完成网络请求自动关闭加载中
  • 自动处理异常信息
  • Cookie自动持久化与Retrofit的协同使用
  • 接口管理Retrofit请求接口的优美方式

一.为什么要封装这套框架

​ 如上一篇文章所说,在MVP模式日渐流行的时候,封装一套MVP框架,不仅对日常的开发大大便利,还能提前积累一下未来在实际工作中的技巧,并且,良好的封装和规范使用还能减少开发中的各种令人头疼的BUG。

​ 有人可能会问:“你上一篇不是也写了MVP框架吗?你这篇难道还是一样的吗?难道你是换汤不换药吗?”

​ 其实,一开始笔者自以为我上一篇文章封装的MVP框架已经够不错了,但是,在笔者某天看了yechaoa大神玩安卓java的源码后,被其封装的MVP框架的所折服,因此第一时间写这篇文章,想向大家分享下,笔者从中汲取的经验,希望能够帮助到各位!

​ 本文相对带你封装自己的MVP+Retrofit+RxJava2框架(一)的改进地方有下面几点:

  • 精简了Activity基类,将原来的两个BaseActivityBaseMvpActivity精简为一个BaseActivity
  • 修复了当继承了Activity基类,不添加Presenter会导致空指针Bug
  • 添加了网络请求可选择自动显示加载中和自动关闭加载中的功能
  • 添加了自动处理异常信息的功能
  • 封装了一个Bean对象的基类
  • 精简了RxJava的用法,因此可以省去Model类的编写
  • 封装了一个Observer的基类
  • 增添了cookie自动持久化的功能
  • 改进RetrofitService的封装,将Retrofit接口的实例化引入基类

二.核心用法与样例分析

本项目基于Android X 进行构建,完整代码已经上传到我的Github仓库

首先,先给大家介绍下笔者项目的基本结构

项目基本结构

为了给大家模拟带自动获取Cookie的功能,所以笔者设计了一个具有登陆,注册,收藏功能的Demo

在这里特别感谢玩安卓提供的API

Demo截图

笔者在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'
    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'

下面笔者将为大家详细介绍每个类的相关信息

2.1 Base基类

2.1.1 BaseActivity

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();
        }
    }
}

2.1.2 BaseFragment


/**
 * 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();
    }
}

2.1.3 BasePresenter

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();
        }
    }

}

2.1.4 BaseObserver

  • Observer的基类,提供了自动显示和自动隐藏进度条的方法

  • 对内处理了onStartonErroronComplete方法

  • 对外只提供了onSuccessonError方法,符合用户一般使用习惯


/**
 * Description : BaseObserver
 *
 * @author XuCanyou666
 * @date 2020/2/7
 */


public abstract class BaseObserver<T> extends DisposableObserver<T> {

    protected BaseView view;
    private boolean isShowDialog;

    protected BaseObserver(BaseView view) {
        this.view = view;
    }

    /**
     * 带进度条的初始化方法
     *
     * @param view         view
     * @param isShowDialog 是否显示进度条
     */
    protected 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);

}

2.1.5 BaseException

异常的基类


/**
 * 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;
    }

}

2.1.6 BaseBean

实体类的基类,方便处理返回的Json数据,具体的写法需根据每个API而定


/**
 * Description : BaseBean 实体类的基类
 *
 * @author XuCanyou666
 * @date 2020/2/7
 */

public class BaseBean<T> implements Serializable {


    /**
     * data :
     * errorCode : 0
     * errorMsg :
     */

    public int errorCode;
    public String errorMsg;
    public T data;

    public BaseBean(int code, String data) {
        this.errorCode = code;
        this.data = (T) data;
    }

}

2.1.7 BaseView


/**
 * Description : BaseView
 *
 * @author XuCanyou666
 * @date 2020/2/7
 */


public interface BaseView {

    void showLoading();

    void hideLoading();

    void onErrorCode(BaseBean bean);

}

2.2 http

2.2.1 cookie

持久化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);
    }
}

2.2.2 gson

重写ResponseBodyConverterJson预处理,这里只展示一个类的代码,详细代码请前往我的Github



/**
 * Created by yechao on 2019/11/18/018.
 * Describe : 重写ResponseBodyConverter对json预处理
 */
public class BaseResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final TypeAdapter<T> adapter;

    /**
     * 登陆失效
     */
    private static final int LOG_OUT_TIME = -1001;

    BaseResponseBodyConverter( TypeAdapter<T> adapter) {
        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 == LOG_OUT_TIME) {
                    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();
        }
    }
}

2.2.3 API

  • 原因:随着项目日渐庞大,请求也越来越多,不可能每个请求都使用一个接口,否则不但造成浪费,而且不方便管理
  • 作用:新建一个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);


    }

}

2.2.4 RetrofitService

Retrofit的配置类,在里面初始化了apiServer对象,并配置了日志信息,超时时间,Cookie持久化,用了静态内部类的单例模式


/**
 * Description : RetrofitService
 *
 * @author XuCanyou666
 * @date 2020/2/8
 */


public class RetrofitService {

    private volatile static RetrofitService apiRetrofit;
    private API.WAZApi apiServer;

    /**
     * 单例调用
     *
     * @return RetrofitService
     */
    public static RetrofitService getInstance() {
        if (apiRetrofit == null) {
            synchronized (Object.class) {
                if (apiRetrofit == null) {
                    apiRetrofit = new RetrofitService();
                }
            }
        }
        return apiRetrofit;
    }


    /**
     * 获取api对象
     *
     * @return 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(XUtil.getApplication()))
                .build();

        //关联okHttp并加上rxJava和Gson的配置和baseUrl
        Retrofit retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(BaseConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(API.BASE_URL)
                .build();

        apiServer = retrofit.create(API.WAZApi.class);
    }

}

2.3 bean

  • 这里的带有嵌套的实体类看似很复杂,其实可以通过ASGsonFormat插件一键生成
  • 注意:不要将data,errorCode,errorMsg的导入到实体类中

2.3.1 Article

文章内容的实体类



/**
 * 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;
    }

}

2.3.2 User



/**
 * 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;

}

2.4 module

  • 这里分模块化进行管理,本DemoLogin,Register,Home总共三个模块
  • 限于篇幅,在这里仅说明一个模块,其他模块的写法类似,具体写法,可以上Github查看

2.4.1 login

2.4.1.1 ILoginView

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);

}
2.4.1.2 LoginPresenter

这里因为RxJava经过封装后,Model层的代码比较精简,所以将Model直接写入Presenter中,以节省工作量



/**
 * Description : LoginPresenter
 *
 * @author XuCanyou666
 * @date 2020/2/8
 */


class LoginPresenter extends BasePresenter<ILoginView> {

    LoginPresenter(ILoginView baseView) {
        super(baseView);
    }


    /**
     * 登陆
     *
     * @param username         username
     * @param password         password
     * @param usernameCountMax 账号规定输入字符最大值
     * @param passwordCountMax 密码规定输入字符最大值
     */
    void login(String username, String password, int usernameCountMax, int passwordCountMax) {
        YUtils.closeSoftKeyboard();
        //判断输入的账号密码是否符合规范
        if (isValid(username, password, usernameCountMax, passwordCountMax)) {
            addDisposable(apiServer.login(username, password), new BaseObserver<BaseBean<User>>(baseView, true) {
                @Override
                public void onSuccess(BaseBean<User> bean) {
                    baseView.showLoginSuccess("登录成功( ̄▽ ̄)");
                    //将登陆的账号存进sp里面
                    SpUtil.setBoolean(GlobalConstant.IS_LOGIN, true);
                    SpUtil.setString(GlobalConstant.USERNAME, bean.data.username);
                    SpUtil.setString(GlobalConstant.PASSWORD, bean.data.password);
                    baseView.doSuccess();
                }

                @Override
                public void onError(String msg) {
                    baseView.showLoginFailed(msg + "(°∀°)ノ");
                }
            });
        } else {
            baseView.showLoginFailed("填写错误 (°∀°)ノ");
        }

    }


    /**
     * 判断输入的账号密码是否符合规范
     *
     * @param userName         username
     * @param password         password
     * @param usernameCountMax 账号规定输入字符最大值
     * @param passwordCountMax 密码规定输入字符最大值
     * @return 是否合规
     */
    private boolean isValid(String userName, String password, int usernameCountMax, int passwordCountMax) {
        return check(userName, usernameCountMax) && check(password, passwordCountMax);
    }

    /**
     * 判断输入是否规范
     *
     * @param string              输入的内容
     * @param tilCounterMaxLength textInputLayout控件的输入字符的最大长度
     * @return 是否合规
     */
    private boolean check(String string, int tilCounterMaxLength) {
        return !TextUtils.isEmpty(string) && string.length() <= tilCounterMaxLength && tilCounterMaxLength / 2 <= string.length();
    }
}

2.4.1.3 LoginTextWatcher

登陆界面输入框的监听器


/**
 * 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);
            }
        }
    }

}
2.4.1.4 LoginActivity

/**
 * 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;


    @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() {
        startActivity(MainActivity.class, true);
    }


    @OnClick({R.id.btn_login, R.id.btn_register})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_login:
                String username = mEtUsername.getText().toString().trim();
                String password = mEtPassword.getText().toString().trim();
                int tilUsernameCounterMaxLength = mTilUsername.getCounterMaxLength();
                int tilPasswordCounterMaxLength = mTilPassword.getCounterMaxLength();
                presenter.login(username, password, tilUsernameCounterMaxLength, tilPasswordCounterMaxLength);
                break;
            case R.id.btn_register:
                YUtils.closeSoftKeyboard();
                startActivity(RegisterActivity.class, false);
                break;
            default:
                break;
        }
    }
}

三.我在使用中遇到的问题

3.1 颜色的资源文件出错

有一天, 当我点开我的colors.xml资源文件的时候,发现是下图这个样子

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文件剪切下来,再粘回去。

感觉是ASBUG....我用的AS版本是3.5.1

3.2 导入依赖的时候提示Failed to resolve

Failed to resolve

当然百度了一下,解决方式是:在根目录的build.gradle中添加maven

就是下面这样

image.png

3.3 在进入文章列表界面的时候,进度条不会自动隐藏

发生问题的场景:笔者在Presenter中请求文章列表的数据的时候,会自动显示和隐藏进度条,但请求完文章列表后,不能自动隐藏

经过浏览代码,发现,我的请求文章列表的方法写多了一次,解决方法:只保存onResume里面的一次

四.快捷体验

如果你想更加简单地使用这套框架,笔者特地为您准备了我已经封装好的 MVP 框架,你只需要导入依赖即可享受如上的快捷的开发体验,详情请见 github

github 地址:https://github.com/LoveLifeEveryday/XMvp

五.在项目中运用

image-20201026213715565

看完了,有些读者可能会有疑惑,说得那么牛逼,在项目中运用又是怎样吖,会不会有bug吖!!别急,笔者马上根据这个框架,开发了一个简单易用美观的『玩安卓』!希望能够解决您的困惑hhh

[图片上传失败...(image-74b509-1603720322753)]

image-20201026214013321

如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容