帶你封裝自己的MVP+Retrofit+RxJava2框架(二)

前言

本篇文章是針對(duì)上一篇文章:帶你封裝自己的MVP+Retrofit+RxJava2框架(一)的進(jìn)一步封裝改進(jìn)缨历,建議在看完上一篇文章后粥血,再食用本文效果更佳题山!

本文已經(jīng)收錄到我的Github個(gè)人博客懈糯,歡迎大佬們光臨寒舍:我的GIthub博客

本篇文章需要已經(jīng)具備的知識(shí):

  • MVP的概念和基本使用
  • Retrofit框架的基本使用
  • RxJava2框架的基本使用
  • ButterKnife框架的基本使用
  • Base基類的概念
  • YUtils的簡單使用
  • BaseRecyclerViewAdapterHelper的簡單使用

學(xué)習(xí)清單:

  • Base實(shí)體類的封裝
  • Base異常類的封裝
  • Base觀察者的封裝
  • RxJava線程自動(dòng)調(diào)度的小技巧
  • 進(jìn)行網(wǎng)絡(luò)請(qǐng)求自動(dòng)顯示加載中
  • 完成網(wǎng)絡(luò)請(qǐng)求自動(dòng)關(guān)閉加載中
  • 自動(dòng)處理異常信息
  • Cookie自動(dòng)持久化與Retrofit的協(xié)同使用
  • 接口管理Retrofit請(qǐng)求接口的優(yōu)美方式

一.為什么要封裝這套框架

? 如上一篇文章所說粤蝎,在MVP模式日漸流行的時(shí)候志笼,封裝一套MVP框架乏盐,不僅對(duì)日常的開發(fā)大大便利什乙,還能提前積累一下未來在實(shí)際工作中的技巧被辑,并且燎悍,良好的封裝和規(guī)范使用還能減少開發(fā)中的各種令人頭疼的BUG。

? 有人可能會(huì)問:“你上一篇不是也寫了MVP框架嗎盼理?你這篇難道還是一樣的嗎谈山?難道你是換湯不換藥嗎?”

? 其實(shí)宏怔,一開始筆者自以為我上一篇文章封裝的MVP框架已經(jīng)夠不錯(cuò)了奏路,但是,在筆者某天看了yechaoa大神玩安卓java的源碼后臊诊,被其封裝的MVP框架的所折服鸽粉,因此第一時(shí)間寫這篇文章,想向大家分享下抓艳,筆者從中汲取的經(jīng)驗(yàn)触机,希望能夠幫助到各位!

? 本文相對(duì)帶你封裝自己的MVP+Retrofit+RxJava2框架(一)的改進(jìn)地方有下面幾點(diǎn):

  • 精簡了Activity基類玷或,將原來的兩個(gè)BaseActivityBaseMvpActivity精簡為一個(gè)BaseActivity
  • 修復(fù)了當(dāng)繼承了Activity基類儡首,不添加Presenter會(huì)導(dǎo)致空指針Bug
  • 添加了網(wǎng)絡(luò)請(qǐng)求可選擇自動(dòng)顯示加載中和自動(dòng)關(guān)閉加載中的功能
  • 添加了自動(dòng)處理異常信息的功能
  • 封裝了一個(gè)Bean對(duì)象的基類
  • 精簡了RxJava的用法,因此可以省去Model類的編寫
  • 封裝了一個(gè)Observer的基類
  • 增添了cookie自動(dòng)持久化的功能
  • 改進(jìn)RetrofitService的封裝偏友,將Retrofit接口的實(shí)例化引入基類

二.核心用法與樣例分析

本項(xiàng)目基于Android X 進(jìn)行構(gòu)建蔬胯,完整代碼已經(jīng)上傳到我的Github倉庫

首先,先給大家介紹下筆者項(xiàng)目的基本結(jié)構(gòu)

項(xiàng)目基本結(jié)構(gòu)

為了給大家模擬帶自動(dòng)獲取Cookie的功能,所以筆者設(shè)計(jì)了一個(gè)具有登陸约谈,注冊(cè)笔宿,收藏功能的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'

下面筆者將為大家詳細(xì)介紹每個(gè)類的相關(guān)信息

2.1 Base基類

2.1.1 BaseActivity

BaseActivity相對(duì)于筆者上一個(gè)版本的MVP框架的改進(jìn)之處:

  • 將兩個(gè)基類Activity合并為一個(gè)BaseActivity
  • 在其中封裝了進(jìn)度條的顯示和隱藏的方法

/**
 * 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);
        //設(shè)置豎屏
        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();
        //銷毀時(shí),解除綁定
        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) {
    }

    /**
     * 啟動(dòng)activity
     *
     * @param activity 當(dāng)前活動(dòng)
     * @param isFinish 是否結(jié)束當(dāng)前活動(dòng)
     */
    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中都可以直接調(diào)用
        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();
        //銷毀時(shí)泼橘,解除綁定
        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相對(duì)于筆者上一個(gè)版本的MVP框架的改進(jìn)之處:

  • 將線程的調(diào)度寫入了addDisposable
  • 改寫了addDisposable方法涝动,使得調(diào)用方式更加簡單優(yōu)美


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


public class BasePresenter<V extends BaseView> {

    private CompositeDisposable compositeDisposable;
    public V baseView;

    /**
     * 這個(gè)后面可以直接用   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的基類炬灭,提供了自動(dòng)顯示和自動(dòng)隱藏進(jìn)度條的方法

  • 對(duì)內(nèi)處理了onStart醋粟,onErroronComplete方法

  • 對(duì)外只提供了onSuccessonError方法重归,符合用戶一般使用習(xí)慣


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

    /**
     * 帶進(jìn)度條的初始化方法
     *
     * @param view         view
     * @param isShowDialog 是否顯示進(jìn)度條
     */
    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;
                //回調(diào)到view層 處理 或者根據(jù)項(xiàng)目情況處理
                if (view != null) {
                    // 處理登錄失效 更新
                    view.onErrorCode(new BaseBean(be.getErrorCode(), be.getErrorMsg()));
                } else {
                    onError(be.getErrorMsg());
                }
                //系統(tǒng)異常
            } else {
                if (e instanceof HttpException) {
                    //HTTP錯(cuò)誤
                    be = new BaseException(BaseException.BAD_NETWORK_MSG, e);
                } else if (e instanceof ConnectException || e instanceof UnknownHostException) {
                    //連接錯(cuò)誤
                    be = new BaseException(BaseException.CONNECT_ERROR_MSG, e);
                } else if (e instanceof InterruptedIOException) {
                    //連接超時(shí)
                    be = new BaseException(BaseException.CONNECT_TIMEOUT_MSG, e);
                } else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
                    //解析錯(cuò)誤
                    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 {

    /**
     * 解析數(shù)據(jù)失敗
     */
    public static final String PARSE_ERROR_MSG = "解析數(shù)據(jù)失敗";
    /**
     * 網(wǎng)絡(luò)問題
     */
    public static final String BAD_NETWORK_MSG = "網(wǎng)絡(luò)問題";
    /**
     * 連接錯(cuò)誤
     */
    public static final String CONNECT_ERROR_MSG = "連接錯(cuò)誤";
    /**
     * 連接超時(shí)
     */
    public static final String CONNECT_TIMEOUT_MSG = "連接超時(shí)";
    /**
     * 未知錯(cuò)誤
     */
    public static final String OTHER_MSG = "未知錯(cuò)誤";

    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

實(shí)體類的基類米愿,方便處理返回的Json數(shù)據(jù),具體的寫法需根據(jù)每個(gè)API而定


/**
 * Description : BaseBean 實(shí)體類的基類
 *
 * @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,因?yàn)榇a太多鼻吮,這里只展示一個(gè)類的代碼育苟,詳細(xì)代碼請(qǐng)前往我的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

重寫ResponseBodyConverter對(duì)Json預(yù)處理,這里只展示一個(gè)類的代碼椎木,詳細(xì)代碼請(qǐng)前往我的Github



/**
 * Created by yechao on 2019/11/18/018.
 * Describe : 重寫ResponseBodyConverter對(duì)json預(yù)處理
 */
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;
                //錯(cuò)誤信息
                if (code == LOG_OUT_TIME) {
                    data = "登錄失效违柏,請(qǐng)重新登錄";
                } else {
                    data = object.getString("errorMsg");
                }
                //異常處理
                throw new BaseException(code, data);
            }
            //正確返回整個(gè)json
            return adapter.fromJson(jsonString);

        } catch (JSONException e) {
            e.printStackTrace();
            //數(shù)據(jù)解析異常即json格式有變動(dòng)
            throw new BaseException(BaseException.PARSE_ERROR_MSG);
        } finally {
            value.close();
        }
    }
}

2.2.3 API

  • 原因:隨著項(xiàng)目日漸龐大,請(qǐng)求也越來越多香椎,不可能每個(gè)請(qǐng)求都使用一個(gè)接口漱竖,否則不但造成浪費(fèi),而且不方便管理
  • 作用:新建一個(gè)API作為Retrofit的管理類畜伐,用一個(gè)接口管理所有網(wǎng)絡(luò)請(qǐng)求馍惹,可以有效改善代碼質(zhì)量

/**
 * Description : API
 * 接口的管理類
 *
 * @author XuCanyou666
 * @date 2020/2/7
 */


public class API {

    static final String BASE_URL = "https://www.wanandroid.com/";

    public interface WAZApi {

        //-----------------------【首頁相關(guān)】----------------------


        //首頁文章列表 這里的{}是填入頁數(shù)
        @GET("article/list/{page}/json")
        Observable<BaseBean<Article>> getArticleList(@Path("page") Integer page);


        //-----------------------【登錄注冊(cè)】----------------------

        //登錄
        @FormUrlEncoded
        @POST("user/login")
        Observable<BaseBean<User>> login(@Field("username") String username, @Field("password") String password);

        //注冊(cè)
        @FormUrlEncoded
        @POST("user/register")
        Observable<BaseBean<User>> register(@Field("username") String username, @Field("password") String password, @Field("repassword") String repassword);


        //-----------------------【  收藏  】----------------------

        //收藏站內(nèi)文章
        @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對(duì)象玛界,并配置了日志信息万矾,超時(shí)時(shí)間,Cookie持久化脚仔,用了靜態(tài)內(nèi)部類的單例模式


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


public class RetrofitService {

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

    /**
     * 單例調(diào)用
     *
     * @return RetrofitService
     */
    public static RetrofitService getInstance() {
        if (apiRetrofit == null) {
            synchronized (Object.class) {
                if (apiRetrofit == null) {
                    apiRetrofit = new RetrofitService();
                }
            }
        }
        return apiRetrofit;
    }


    /**
     * 獲取api對(duì)象
     *
     * @return api對(duì)象
     */
    public API.WAZApi getApiService() {
        return apiServer;
    }


    /**
     * 初始化retrofit
     */
    private RetrofitService() {

        //配置okHttp并設(shè)置時(shí)間勤众、日志信息和cookies
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(httpLoggingInterceptor)
                //設(shè)置超時(shí)時(shí)間
                .connectTimeout(15, TimeUnit.SECONDS)
                //設(shè)置Cookie持久化
                .cookieJar(new CookiesManager(XUtil.getApplication()))
                .build();

        //關(guān)聯(lián)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

  • 這里的帶有嵌套的實(shí)體類看似很復(fù)雜,其實(shí)可以通過ASGsonFormat插件一鍵生成
  • 注意:不要將data,errorCode,errorMsg的導(dǎo)入到實(shí)體類中

2.3.1 Article

文章內(nèi)容的實(shí)體類



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


public class Article {

    /**
     * curPage : 2
     * datas : [{"apkLink":"","author":"葉應(yīng)是葉","chapterId":67,"chapterName":"網(wǎng)絡(luò)基......."}]
     * 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 : 葉應(yīng)是葉
         * chapterId : 67
         * chapterName : 網(wǎng)絡(luò)基礎(chǔ)
         * collect : false
         * courseId : 13
         * desc :
         * envelopePic :
         * fresh : false
         * id : 2809
         * link : http://www.reibang.com/p/6d2f324c8f42
         * niceDate : 2018-04-12
         * origin :
         * projectLink :
         * publishTime : 1523532264000
         * superChapterId : 98
         * superChapterName : 網(wǎng)絡(luò)訪問
         * tags : []
         * title : 在 Android 設(shè)備上搭建 Web 服務(wù)器
         * 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

  • 這里分模塊化進(jìn)行管理鲤脏,本DemoLogin,Register,Home總共三個(gè)模塊
  • 限于篇幅们颜,在這里僅說明一個(gè)模塊,其他模塊的寫法類似猎醇,具體寫法窥突,可以上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

這里因?yàn)?code>RxJava經(jīng)過封裝后,Model層的代碼比較精簡硫嘶,所以將Model直接寫入Presenter中阻问,以節(jié)省工作量



/**
 * 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 賬號(hào)規(guī)定輸入字符最大值
     * @param passwordCountMax 密碼規(guī)定輸入字符最大值
     */
    void login(String username, String password, int usernameCountMax, int passwordCountMax) {
        YUtils.closeSoftKeyboard();
        //判斷輸入的賬號(hào)密碼是否符合規(guī)范
        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("登錄成功( ̄▽ ̄)");
                    //將登陸的賬號(hào)存進(jìn)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("填寫錯(cuò)誤 (°?°)?");
        }

    }


    /**
     * 判斷輸入的賬號(hào)密碼是否符合規(guī)范
     *
     * @param userName         username
     * @param password         password
     * @param usernameCountMax 賬號(hào)規(guī)定輸入字符最大值
     * @param passwordCountMax 密碼規(guī)定輸入字符最大值
     * @return 是否合規(guī)
     */
    private boolean isValid(String userName, String password, int usernameCountMax, int passwordCountMax) {
        return check(userName, usernameCountMax) && check(password, passwordCountMax);
    }

    /**
     * 判斷輸入是否規(guī)范
     *
     * @param string              輸入的內(nèi)容
     * @param tilCounterMaxLength textInputLayout控件的輸入字符的最大長度
     * @return 是否合規(guī)
     */
    private boolean check(String string, int tilCounterMaxLength) {
        return !TextUtils.isEmpty(string) && string.length() <= tilCounterMaxLength && tilCounterMaxLength / 2 <= string.length();
    }
}

2.4.1.3 LoginTextWatcher

登陸界面輸入框的監(jiān)聽器


/**
 * TextInputLayout監(jiān)聽器
 * 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);
    }

    /**
     * 判斷輸入內(nèi)容是否合法
     *
     * @param textInputLayout textInputLayout
     */
    public static void checkInput(TextInputLayout textInputLayout) {
        if (textInputLayout != null) {
            if (textInputLayout.getEditText().getText().length() > textInputLayout.getCounterMaxLength()) {
                textInputLayout.setError("輸入內(nèi)容超過上限");
            } 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 顏色的資源文件出錯(cuò)

有一天, 當(dāng)我點(diǎn)開我的colors.xml資源文件的時(shí)候沦疾,發(fā)現(xiàn)是下圖這個(gè)樣子

colors.xml

然后當(dāng)鼠標(biāo)的光標(biāo)移動(dòng)到紅色標(biāo)記處称近,發(fā)現(xiàn)

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中沒有聲明

懵逼了第队,我不是聲明了嗎....最后還是百度到了結(jié)果

解決方式是:先把colors文件剪切下來,再粘回去刨秆。

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

3.2 導(dǎo)入依賴的時(shí)候提示Failed to resolve

Failed to resolve

當(dāng)然百度了一下凳谦,解決方式是:在根目錄的build.gradle中添加maven

就是下面這樣

image.png

3.3 在進(jìn)入文章列表界面的時(shí)候,進(jìn)度條不會(huì)自動(dòng)隱藏

發(fā)生問題的場景:筆者在Presenter中請(qǐng)求文章列表的數(shù)據(jù)的時(shí)候衡未,會(huì)自動(dòng)顯示和隱藏進(jìn)度條尸执,但請(qǐng)求完文章列表后,不能自動(dòng)隱藏

經(jīng)過瀏覽代碼缓醋,發(fā)現(xiàn)如失,我的請(qǐng)求文章列表的方法寫多了一次,解決方法:只保存onResume里面的一次

四.快捷體驗(yàn)

如果你想更加簡單地使用這套框架送粱,筆者特地為您準(zhǔn)備了我已經(jīng)封裝好的 MVP 框架褪贵,你只需要導(dǎo)入依賴即可享受如上的快捷的開發(fā)體驗(yàn),詳情請(qǐng)見 github

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

五.在項(xiàng)目中運(yùn)用

image-20201026213715565

看完了抗俄,有些讀者可能會(huì)有疑惑竭鞍,說得那么牛逼,在項(xiàng)目中運(yùn)用又是怎樣吖橄镜,會(huì)不會(huì)有bug吖!冯乘!別急洽胶,筆者馬上根據(jù)這個(gè)框架,開發(fā)了一個(gè)簡單易用美觀的『玩安卓』裆馒!希望能夠解決您的困惑hhh

[圖片上傳失敗...(image-74b509-1603720322753)]

image-20201026214013321

如果文章對(duì)您有一點(diǎn)幫助的話姊氓,希望您能點(diǎn)一下贊,您的點(diǎn)贊喷好,是我前進(jìn)的動(dòng)力

本文參考鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翔横,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子梗搅,更是在濱河造成了極大的恐慌禾唁,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件无切,死亡現(xiàn)場離奇詭異荡短,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)哆键,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門掘托,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人籍嘹,你說我怎么就攤上這事闪盔⊥湓海” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵泪掀,是天一觀的道長听绳。 經(jīng)常有香客問我,道長族淮,這世上最難降的妖魔是什么辫红? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任祝辣,我火速辦了婚禮贴妻,結(jié)果婚禮上蝙斜,老公的妹妹穿的比我還像新娘名惩。我一直安慰自己孕荠,他們只是感情好娩鹉,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稚伍,像睡著了一般弯予。 火紅的嫁衣襯著肌膚如雪个曙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天垦搬,我揣著相機(jī)與錄音呼寸,去河邊找鬼。 笑死猴贰,一個(gè)胖子當(dāng)著我的面吹牛对雪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瑟捣,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼栅干,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蝶柿!你這毒婦竟也來了非驮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎星岗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戒洼,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年寥掐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磷蜀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片召耘。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡褐隆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庶弃,到底是詐尸還是另有隱情,我是刑警寧澤歇攻,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站缝呕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摊聋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望麻裁。 院中可真熱鬧,春花似錦煎源、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诈悍。三九已至祸轮,卻和暖如春侥钳,著一層夾襖步出監(jiān)牢的瞬間适袜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工舷夺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苦酱,地道東北人给猾。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像耙册,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子详拙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容