天天看點

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

開篇

        一個APP,它所涉及到的内容不隻是APP表面這些界面、元素的beautiful和開發。APP之是以為APP它不同于VUE JS、微信小程式,一個APP它内部也分成多個層次,有純前端、有Adapter、有模型層、有API層、有底層、有緩存、有内嵌存儲。

        一個APP其實是一個完整的企業級架構體系,是以APP上才需要架構師,是以APP在業界并沒有歸到“純前端”,而叫“大前端”,它是自身獨立的一整套體系化知識所組成。

        特别是近幾年來,随着智能手機在市場上進一步普及後APP已經不僅僅隻是人們以前認知上一個簡單的“前端”了。它也已經開始區分出:前端、中間件、緩存、後端、資料庫、網絡互動、裝置互動、動畫這些幾個主要大科。在這些大科下再衍生出上百個門類的知識點。

        特别是裝置互動和網絡互動,HTTP/RESULTFUL API互動其實也屬于底層。是以作為大前端,架構上非單一層式傳統設計時我們經常會進行思考:即我們往往也希望可以像進階語言一樣把一些底層features“掩蓋”住以加速程式員開發效率。

        是以,今天我們就要講時下最流行的retrofit2+okhttp3+rxjava封裝的http/res㔞 api調用原理。這3個東西組合比較複雜,但是我相信跟着我的教程一步步來絕對保證你一次成功。

為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事

        為什麼要從“簡”變“難”、擇“難”去“易”呢?

        當然,這是“碼農”的眼光去看待原來一個okhttp3可以搞定的事的角度去思考這件事所能看到的局限。而一旦讓我們從全局來看原來所謂“一個okhttp3”可以搞定的事到底是個什麼樣的情況呢?

        假設,我們有一個get請求,傳回是一個{"code":0, "data":"4e23h100hm5ffg8l"}這樣的一restful api,我們用okhttp3是怎麼實作的呢?

  1. 先要定義http請求頭;
  1. 設定一堆的請求變量;
  1. 聲明okhttp builder為同步還是異步;
  1. 覆寫on success, on fail等方法;
  1. 判斷http傳回值是否200;
  1. 如果是200,取body;
  1. body取到後轉成gson;
  1. 然後。。。
  1. 然後。。。
  1. 還有異步線程handler回調;
  1. 重新整理界面;

        每一個不同的API請求都是上面這一陀。。。對不?然後有人說了。。。你封裝呀,把一些重複步驟封裝呀。。。哈哈哈。

        說到點了,retrofit+okhttp3+rxjava就是這個封裝,當然它不可能封裝到開發、程式員們隻需要一句話就能實作上面所有步驟,這是不現實的。

        但是retrofit+okhttp3+rxjava可以把以上一堆步驟中的重複書寫代碼進行了最大程度的封裝。。。so。。。已經有“大牛”給你封裝了”,你自己也就不用去造輪子了。這就是我在另一個架構系列-高性能零售架構系列裡提到的:當團隊超過幾十人且屬于規模化基于一個産品進行協同作戰模式下,根本不需要去做什麼封裝。因為你是對企業的穩定性、性能負責而不是快點交了可以收合同尾款這種乙方性質作法時,你會誕生一種這樣的想法:希望這個東西應封裝盡封裝,不該封裝的就不要封,在解決問題、共享資訊、消除黑盒、解放生産力、品質、性能上達到最大的balance。這是一種“企業級架構思想”。

        retrofit+okhttp3+rxjava正是這麼一種存在,這也是它存在的意義所在即:給予你充分自由又可最大限度消除一些重複代碼,并且還掩蓋了底層協定部分内容充分解放了程式員的生産力。

        看看似乎有三樣東西一起做,其實使用上你是無感的。

        筆者基于這3個組合上,做了少許的小封裝,使得它幾乎可以适用于任何的開發團隊,同時在筆者自己的mao-sir.com上也是使用得是這一套東西,架構代碼完全公開給大家。

        下面就讓我們一起進入retrofit+okhttp3+rxjava的架構搭建教程。

搭建retrofit2+okhttp3+rxjava

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

        以上就是mao-sir的retrofit2+okhttp3+rxjava的架構的結構。

        任何一個業務子產品裡隻要寫一個xxx.xxx.api.業務名API就可以使用restful api的通路了。

        看看挺多,其實一點沒什麼難度,我們一塊塊把它們“掰開揉碎”了一點點來消化即可。

引入retrofit2+okhttp3+rxjava第三方包

        在子產品的build.gradle裡引入下面這些包

    //retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    //日志攔截器
    implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    //rxjava
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
    //gson
    implementation 'com.google.code.gson:gson:2.8.7'
           

        引入後它長這樣

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

http協定層相關類封裝

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

ExceptionHandle.java

package com.mkyuan.aset.mall.android.network.errorhandler;

import com.google.gson.JsonParseException;

import org.apache.http.conn.ConnectTimeoutException;
import org.json.JSONException;

import java.net.ConnectException;
import java.text.ParseException;

import retrofit2.HttpException;

/**
 * 異常處理
 * @author mk.yuan
 */
public class ExceptionHandle {
    //未授權
    private static final int UNAUTHORIZED = 401;
    //禁止的
    private static final int FORBIDDEN = 403;
    //未找到
    private static final int NOT_FOUND = 404;
    //請求逾時
    private static final int REQUEST_TIMEOUT = 408;
    //内部伺服器錯誤
    private static final int INTERNAL_SERVER_ERROR = 500;
    //錯誤網關
    private static final int BAD_GATEWAY = 502;
    //暫停服務
    private static final int SERVICE_UNAVAILABLE = 503;
    //網關逾時
    private static final int GATEWAY_TIMEOUT = 504;

    /**
     * 處理異常
     * @param throwable
     * @return
     */
    public static ResponseThrowable handleException(Throwable throwable) {
        //傳回時抛出異常
        ResponseThrowable responseThrowable;
        if (throwable instanceof HttpException) {
            HttpException httpException = (HttpException) throwable;
            responseThrowable = new ResponseThrowable(throwable, ERROR.HTTP_ERROR);
            switch (httpException.code()) {
                case UNAUTHORIZED:
                    responseThrowable.message = "未授權";
                    break;
                case FORBIDDEN:
                    responseThrowable.message = "禁止通路";
                    break;
                case NOT_FOUND:
                    responseThrowable.message = "未找到";
                    break;
                case REQUEST_TIMEOUT:
                    responseThrowable.message = "請求逾時";
                    break;
                case GATEWAY_TIMEOUT:
                    responseThrowable.message = "網關逾時";
                    break;
                case INTERNAL_SERVER_ERROR:
                    responseThrowable.message = "内部伺服器錯誤";
                    break;
                case BAD_GATEWAY:
                    responseThrowable.message = "錯誤網關";
                    break;
                case SERVICE_UNAVAILABLE:
                    responseThrowable.message = "暫停服務";
                    break;
                default:
                    responseThrowable.message = "網絡錯誤";
                    break;
            }
            return responseThrowable;
        } else if (throwable instanceof ServerException) {
            //伺服器異常
            ServerException resultException = (ServerException) throwable;
            responseThrowable = new ResponseThrowable(resultException, resultException.code);
            responseThrowable.message = resultException.message;
            return responseThrowable;
        } else if (throwable instanceof JsonParseException
                || throwable instanceof JSONException
                || throwable instanceof ParseException) {
            responseThrowable = new ResponseThrowable(throwable, ERROR.PARSE_ERROR);
            responseThrowable.message = "解析錯誤";
            return responseThrowable;
        } else if (throwable instanceof ConnectException) {
            responseThrowable = new ResponseThrowable(throwable, ERROR.NETWORK_ERROR);
            responseThrowable.message = "連接配接失敗";
            return responseThrowable;
        } else if (throwable instanceof javax.net.ssl.SSLHandshakeException) {
            responseThrowable = new ResponseThrowable(throwable, ERROR.SSL_ERROR);
            responseThrowable.message = "證書驗證失敗";
            return responseThrowable;
        } else if (throwable instanceof ConnectTimeoutException){
            responseThrowable = new ResponseThrowable(throwable, ERROR.TIMEOUT_ERROR);
            responseThrowable.message = "連接配接逾時";
            return responseThrowable;
        } else if (throwable instanceof java.net.SocketTimeoutException) {
            responseThrowable = new ResponseThrowable(throwable, ERROR.TIMEOUT_ERROR);
            responseThrowable.message = "連接配接逾時";
            return responseThrowable;
        }
        else {
            responseThrowable = new ResponseThrowable(throwable, ERROR.UNKNOWN);
            responseThrowable.message = "未知錯誤";
            return responseThrowable;
        }
    }


    /**
     * 約定異常
     */
    public class ERROR {
        /**
         * 未知錯誤
         */
        public static final int UNKNOWN = 1000;
        /**
         * 解析錯誤
         */
        public static final int PARSE_ERROR = 1001;
        /**
         * 網絡錯誤
         */
        public static final int NETWORK_ERROR = 1002;
        /**
         * 協定出錯
         */
        public static final int HTTP_ERROR = 1003;

        /**
         * 證書出錯
         */
        public static final int SSL_ERROR = 1005;

        /**
         * 連接配接逾時
         */
        public static final int TIMEOUT_ERROR = 1006;
    }

    public static class ResponseThrowable extends Exception {
        public int code;
        public String message;

        public ResponseThrowable(Throwable throwable, int code) {
            super(throwable);
            this.code = code;
        }
    }

    public static class ServerException extends RuntimeException {
        public int code;
        public String message;
    }
}
           

HttpErrorHandler.java

package com.mkyuan.aset.mall.android.network.errorhandler;

import io.reactivex.Observable;
import io.reactivex.functions.Function;

/**
 * 網絡錯誤處理
 * @author mk.yuan
 */
public class HttpErrorHandler<T> implements Function<Throwable, Observable<T>> {

    /**
     * 處理以下兩類網絡錯誤:
     * 1、http請求相關的錯誤,例如:404,403,socket timeout等等;
     * 2、應用資料的錯誤會抛RuntimeException,最後也會走到這個函數來統一處理;
     */
    @Override
    public Observable<T> apply(Throwable throwable) throws Exception {
        //通過這個異常處理,得到使用者可以知道的原因
        return Observable.error(ExceptionHandle.handleException(throwable));
    }
}
           

RequestInterceptor.java

package com.mkyuan.aset.mall.android.network.interceptor;

import com.mkyuan.aset.mall.android.network.INetworkRequiredInfo;
import com.mkyuan.aset.mall.android.network.util.DateUtil;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
 * 請求攔截器
 * @author mk.yuan
 */
public class RequestInterceptor implements Interceptor {
    /**
     * 網絡請求資訊
     */
    private INetworkRequiredInfo iNetworkRequiredInfo;

    public RequestInterceptor(INetworkRequiredInfo iNetworkRequiredInfo){
        this.iNetworkRequiredInfo = iNetworkRequiredInfo;
    }

    /**
     * 攔截
     * @return
     */
    @Override
    public Response intercept(Chain chain) throws IOException {
        String nowDateTime = DateUtil.getDateTime();
        //建構器
        Request.Builder builder = chain.request().newBuilder();
        //添加使用環境
        builder.addHeader("os","android");
        //添加版本号
        builder.addHeader("appVersionCode",this.iNetworkRequiredInfo.getAppVersionCode());
        //添加版本名
        builder.addHeader("appVersionName",this.iNetworkRequiredInfo.getAppVersionName());
        //添加日期時間
        builder.addHeader("datetime",nowDateTime);
        //傳回
        return chain.proceed(builder.build());
    }
}
           

ResponseInterceptor.java

package com.mkyuan.aset.mall.android.network.interceptor;


import com.mkyuan.aset.mall.android.network.util.KLog;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Response;

/**
 * 傳回攔截器(響應攔截器)
 *
 * @author mk.yuan
 */
public class ResponseInterceptor implements Interceptor {

    private static final String TAG = "DemoOkRetroRx";

    /**
     * 攔截
     */
    @Override
    public Response intercept(Chain chain) throws IOException {
        long requestTime = System.currentTimeMillis();
        Response response = chain.proceed(chain.request());
        KLog.i(TAG, ">>>>>>requestSpendTime=" + (System.currentTimeMillis() - requestTime) + "ms");
        return response;
    }
}
           

        上面協定層可以自己把有需要的http error繼續加入進去。上述兩個類的作用就是減少了你項目裡在restful api調用時那些出錯啦、錯誤碼啦怎麼處理的一個統一封裝,類似我們在spring boot書寫時有一個統一的controller層處理抛出exception的AOP機制一樣的原理。

攔截http請求、傳回封包并輸出日志用工具類

        這個類屬于輔助類,因為當有了retrofit2+okhttp3+rxjava後,你在你的代碼裡是看不到類似request過去、get/put/post、response回來這些細節了,那麼retrofit2隻是對okhttp3的一層包裝,我們還是希望看到封裝過的restful http請求過時是什麼封包、傳回時是什麼封包這些原始資料以便于調試,是以我們就需要做一個日志工具類來列印和記錄這些資訊,它好比restful client調試http restful請求時當一條請求發送後,工具的右下角會出現一條:curl語句是一樣的作用和目的。

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

KLog.java

package com.mkyuan.aset.mall.android.network.util;

import android.text.TextUtils;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * 自定義日志類
 */
public final class KLog {

    private static boolean IS_SHOW_LOG = true;

    private static final String DEFAULT_MESSAGE = "execute";
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    private static final int JSON_INDENT = 4;

    private static final int V = 0x1;
    private static final int D = 0x2;
    private static final int I = 0x3;
    private static final int W = 0x4;
    private static final int E = 0x5;
    private static final int A = 0x6;
    private static final int JSON = 0x7;

    public static void init(boolean isShowLog) {
        IS_SHOW_LOG = isShowLog;
    }

    public static void v() {
        printLog(V, null, DEFAULT_MESSAGE);
    }

    public static void v(String msg) {
        printLog(V, null, msg);
    }

    public static void v(String tag, String msg) {
        printLog(V, tag, msg);
    }

    public static void d() {
        printLog(D, null, DEFAULT_MESSAGE);
    }

    public static void d(String msg) {
        printLog(D, null, msg);
    }

    public static void d(String tag, String msg) {
        printLog(D, tag, msg);
    }

    public static void i() {
        printLog(I, null, DEFAULT_MESSAGE);
    }

    public static void i(String msg) {
        printLog(I, null, msg);
    }

    public static void i(String tag, String msg) {
        printLog(I, tag, msg);
    }

    public static void w() {
        printLog(W, null, DEFAULT_MESSAGE);
    }

    public static void w(String msg) {
        printLog(W, null, msg);
    }

    public static void w(String tag, String msg) {
        printLog(W, tag, msg);
    }

    public static void e() {
        printLog(E, null, DEFAULT_MESSAGE);
    }

    public static void e(String msg) {
        printLog(E, null, msg);
    }

    public static void e(String tag, String msg) {
        printLog(E, tag, msg);
    }

    public static void a() {
        printLog(A, null, DEFAULT_MESSAGE);
    }

    public static void a(String msg) {
        printLog(A, null, msg);
    }

    public static void a(String tag, String msg) {
        printLog(A, tag, msg);
    }


    public static void json(String jsonFormat) {
        printLog(JSON, null, jsonFormat);
    }

    public static void json(String tag, String jsonFormat) {
        printLog(JSON, tag, jsonFormat);
    }


    private static void printLog(int type, String tagStr, String msg) {

        if (!IS_SHOW_LOG) {
            return;
        }

        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();

        int index = 4;
        String className = stackTrace[index].getFileName();
        String methodName = stackTrace[index].getMethodName();
        int lineNumber = stackTrace[index].getLineNumber();

        String tag = (tagStr == null ? className : tagStr);
        methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[ (").append(className).append(":").append(lineNumber).append(")#").append(methodName).append(" ] ");

        if (msg != null && type != JSON) {
            stringBuilder.append(msg);
        }

        String logStr = stringBuilder.toString();

        switch (type) {
            case V:
                Log.v(tag, logStr);
                break;
            case D:
                Log.d(tag, logStr);
                break;
            case I:
                Log.i(tag, logStr);
                break;
            case W:
                Log.w(tag, logStr);
                break;
            case E:
                Log.e(tag, logStr);
                break;
            case A:
                Log.wtf(tag, logStr);
                break;
            case JSON: {

                if (TextUtils.isEmpty(msg)) {
                    Log.d(tag, "Empty or Null json content");
                    return;
                }

                String message = null;

                try {
                    if (msg.startsWith("{")) {
                        JSONObject jsonObject = new JSONObject(msg);
                        message = jsonObject.toString(JSON_INDENT);
                    } else if (msg.startsWith("[")) {
                        JSONArray jsonArray = new JSONArray(msg);
                        message = jsonArray.toString(JSON_INDENT);
                    }
                } catch (JSONException e) {
                    e(tag, e.getCause().getMessage() + "\n" + msg);
                    return;
                }

                printLine(tag, true);
                message = logStr + LINE_SEPARATOR + message;
                String[] lines = message.split(LINE_SEPARATOR);
                StringBuilder jsonContent = new StringBuilder();
                for (String line : lines) {
                    jsonContent.append("║ ").append(line).append(LINE_SEPARATOR);
                }
                Log.d(tag, jsonContent.toString());
                printLine(tag, false);
            }
            break;
            default:
                break;
        }

    }

    private static void printLine(String tag, boolean isTop) {
        if (isTop) {
            Log.d(tag, "╔═══════════════════════════════════════════════════════════════════════════════════════");
        } else {
            Log.d(tag, "╚═══════════════════════════════════════════════════════════════════════════════════════");
        }
    }
}

           

        它裡面還用到了一個輔助類,DataUtil類,代碼也附上。

DataUtil.java

package com.mkyuan.aset.mall.android.network.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Objects;

public class DateUtil {
    public static final String STANDARD_TIME = "yyyy-MM-dd HH:mm:ss";
    public static final String FULL_TIME = "yyyy-MM-dd HH:mm:ss.SSS";
    public static final String YEAR_MONTH_DAY = "yyyy-MM-dd";
    public static final String YEAR_MONTH_DAY_CN = "yyyy年MM月dd号";
    public static final String HOUR_MINUTE_SECOND = "HH:mm:ss";
    public static final String HOUR_MINUTE_SECOND_CN = "HH時mm分ss秒";
    public static final String YEAR = "yyyy";
    public static final String MONTH = "MM";
    public static final String DAY = "dd";
    public static final String HOUR = "HH";
    public static final String MINUTE = "mm";
    public static final String SECOND = "ss";
    public static final String MILLISECOND = "SSS";
    public static final String YESTERDAY = "昨天";
    public static final String TODAY = "今天";
    public static final String TOMORROW = "明天";
    public static final String SUNDAY = "星期日";
    public static final String MONDAY = "星期一";
    public static final String TUESDAY = "星期二";
    public static final String WEDNESDAY = "星期三";
    public static final String THURSDAY = "星期四";
    public static final String FRIDAY = "星期五";
    public static final String SATURDAY = "星期六";
    public static final String[] weekDays = {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};

    /**
     * 擷取标準時間
     *
     * @return 例如 2021-07-01 10:35:53
     */
    public static String getDateTime() {
        return new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取完整時間
     *
     * @return 例如 2021-07-01 10:37:00.748
     */
    public static String getFullDateTime() {
        return new SimpleDateFormat(FULL_TIME, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取年月日(今天)
     *
     * @return 例如 2021-07-01
     */
    public static String getTheYearMonthAndDay() {
        return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取年月日
     *
     * @return 例如 2021年07月01号
     */
    public static String getTheYearMonthAndDayCn() {
        return new SimpleDateFormat(YEAR_MONTH_DAY_CN, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取年月日
     * @param delimiter 分隔符
     * @return 例如 2021年07月01号
     */
    public static String getTheYearMonthAndDayDelimiter(CharSequence delimiter) {
        return new SimpleDateFormat(YEAR + delimiter + MONTH + delimiter + DAY, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取時分秒
     *
     * @return 例如 10:38:25
     */
    public static String getHoursMinutesAndSeconds() {
        return new SimpleDateFormat(HOUR_MINUTE_SECOND, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取時分秒
     *
     * @return 例如 10時38分50秒
     */
    public static String getHoursMinutesAndSecondsCn() {
        return new SimpleDateFormat(HOUR_MINUTE_SECOND_CN, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取時分秒
     * @param delimiter 分隔符
     * @return 例如 2021/07/01
     */
    public static String getHoursMinutesAndSecondsDelimiter(CharSequence delimiter) {
        return new SimpleDateFormat(HOUR + delimiter + MINUTE + delimiter + SECOND, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取年
     *
     * @return 例如 2021
     */
    public static String getYear() {
        return new SimpleDateFormat(YEAR, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取月
     *
     * @return 例如 07
     */
    public static String getMonth() {
        return new SimpleDateFormat(MONTH, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取天
     *
     * @return 例如 01
     */
    public static String getDay() {
        return new SimpleDateFormat(DAY, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取小時
     *
     * @return 例如 10
     */
    public static String getHour() {
        return new SimpleDateFormat(HOUR, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取分鐘
     *
     * @return 例如 40
     */
    public static String getMinute() {
        return new SimpleDateFormat(MINUTE, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取秒
     *
     * @return 例如 58
     */
    public static String getSecond() {
        return new SimpleDateFormat(SECOND, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取毫秒
     *
     * @return 例如 666
     */
    public static String getMilliSecond() {
        return new SimpleDateFormat(MILLISECOND, Locale.CHINESE).format(new Date());
    }

    /**
     * 擷取時間戳
     *
     * @return 例如 1625107306051
     */
    public static long getTimestamp() {
        return System.currentTimeMillis();
    }

    /**
     * 将時間轉換為時間戳
     *
     * @param time 例如 2021-07-01 10:44:11
     * @return 1625107451000
     */
    public static long dateToStamp(String time) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE);
        Date date = null;
        try {
            date = simpleDateFormat.parse(time);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return Objects.requireNonNull(date).getTime();
    }

    /**
     * 将時間戳轉換為時間
     *
     * @param timeMillis 例如 1625107637084
     * @return 例如 2021-07-01 10:47:17
     */
    public static String stampToDate(long timeMillis) {
        return new SimpleDateFormat(STANDARD_TIME, Locale.CHINESE).format(new Date(timeMillis));
    }

    /**
     * 擷取今天是星期幾
     *
     * @return 例如 星期四
     */
    public static String getTodayOfWeek() {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        int index = cal.get(Calendar.DAY_OF_WEEK) - 1;
        if (index < 0) {
            index = 0;
        }
        return weekDays[index];
    }

    /**
     * 根據輸入的日期時間計算是星期幾
     *
     * @param dateTime 例如 2021-06-20
     * @return 例如 星期日
     */
    public static String getWeek(String dateTime) {
        Calendar cal = Calendar.getInstance();
        if ("".equals(dateTime)) {
            cal.setTime(new Date(System.currentTimeMillis()));
        } else {
            SimpleDateFormat sdf = new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault());
            Date date;
            try {
                date = sdf.parse(dateTime);
            } catch (ParseException e) {
                date = null;
                e.printStackTrace();
            }
            if (date != null) {
                cal.setTime(new Date(date.getTime()));
            }
        }
        return weekDays[cal.get(Calendar.DAY_OF_WEEK) - 1];
    }

    /**
     * 擷取輸入日期的昨天
     *
     * @param date 例如 2021-07-01
     * @return 例如 2021-06-30
     */
    public static String getYesterday(Date date) {
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.add(Calendar.DATE, -1);
        date = calendar.getTime();
        return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date);
    }

    /**
     * 擷取輸入日期的明天
     *
     * @param date 例如 2021-07-01
     * @return 例如 2021-07-02
     */
    public static String getTomorrow(Date date) {
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.add(Calendar.DATE, +1);
        date = calendar.getTime();
        return new SimpleDateFormat(YEAR_MONTH_DAY, Locale.getDefault()).format(date);
    }

    /**
     * 根據年月日計算是星期幾并與目前日期判斷  非昨天、今天、明天 則以星期顯示
     *
     * @param dateTime 例如 2021-07-03
     * @return 例如 星期六
     */
    public static String getDayInfo(String dateTime) {
        String dayInfo;
        String yesterday = getYesterday(new Date());
        String today = getTheYearMonthAndDay();
        String tomorrow = getTomorrow(new Date());

        if (dateTime.equals(yesterday)) {
            dayInfo = YESTERDAY;
        } else if (dateTime.equals(today)) {
            dayInfo = TODAY;
        } else if (dateTime.equals(tomorrow)) {
            dayInfo = TOMORROW;
        } else {
            dayInfo = getWeek(dateTime);
        }
        return dayInfo;
    }

    /**
     * 擷取本月天數
     *
     * @return 例如 31
     */
    public static int getCurrentMonthDays() {
        Calendar calendar = Calendar.getInstance();
        //把日期設定為當月第一天
        calendar.set(Calendar.DATE, 1);
        //日期復原一天,也就是最後一天
        calendar.roll(Calendar.DATE, -1);
        return calendar.get(Calendar.DATE);
    }

    /**
     * 獲得指定月的天數
     *
     * @param year  例如 2021
     * @param month 例如 7
     * @return 例如 31
     */
    public static int getMonthDays(int year, int month) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month - 1);
        //把日期設定為當月第一天
        calendar.set(Calendar.DATE, 1);
        //日期復原一天,也就是最後一天
        calendar.roll(Calendar.DATE, -1);
        return calendar.get(Calendar.DATE);
    }
}
           

rxjava封裝response成object

        rxjava采用的是Observer模式,它可以讓你的http請求體在傳回時采用異步懶加載的方式,進而不影響到Android的主程序而引起相應的Android崩潰、閃退等問題。

        在retrofit+okhttp+rxjava架構裡,三者的分工其實是這樣的:

  • Retrofit做上層網絡請求接口的封裝,同時将需要的資料解析成實體;
  • OkHttp負責請求的過程;
  • RxJava負責異步和線程切換;
Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

BaseResponse.java

package com.mkyuan.aset.mall.android.network;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

/**
 * 基礎傳回類
 * @author mk.yuan
 */
public class BaseResponse {

    //傳回碼
    @SerializedName("res_code")
    @Expose
    public Integer responseCode;

    //傳回的錯誤資訊
    @SerializedName("res_error")
    @Expose
    public String responseError;
}
           

BaseObserver.java

package com.mkyuan.aset.mall.android.network;

import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;

/**
 * 自定義Observer
 *
 * @author mk.yuan
 */
public abstract class BaseObserver<T> implements Observer<T> {

    //開始
    @Override
    public void onSubscribe(Disposable disposable) {

    }

    //繼續
    @Override
    public void onNext(T t) {
        onSuccess(t);
    }

    //異常
    @Override
    public void onError(Throwable e) {
        onFailure(e);
    }

    //完成
    @Override
    public void onComplete() {

    }

    //成功
    public abstract void onSuccess(T t);

    //失敗
    public abstract void onFailure(Throwable e);
}
           

        此處請一定記得這個Observer要使用io.reactivex下的Observer。

使用retrofit對于okhttp3的網絡請求的封裝

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

INetworkRequiredInfo.java

package com.mkyuan.aset.mall.android.network;

import android.app.Application;

public interface  INetworkRequiredInfo {
    /**
     * 擷取App版本名
     */
    String getAppVersionName();

    /**
     * 擷取App版本号
     */
    String getAppVersionCode();

    /**
     * 判斷是否為Debug模式
     */
    boolean isDebug();

    /**
     * 擷取全局上下文參數
     */
    Application getApplicationContext();
}
           

NetworkRequiredInfo.java

        它是INetworkRequiredInfo的實作類。

package com.mkyuan.aset.mall.android.network;

import android.app.Application;

import com.mkyuan.aset.mall.android.BuildConfig;


/**
 * 網絡通路資訊
 * @author mk.yuan
 */
public class NetworkRequiredInfo implements INetworkRequiredInfo {

    private final Application application;

    public NetworkRequiredInfo(Application application){
        this.application = application;
    }

    /**
     * 版本名
     */
    @Override
    public String getAppVersionName() {
        return BuildConfig.VERSION_NAME;
    }
    /**
     * 版本号
     */
    @Override
    public String getAppVersionCode() {
        return String.valueOf(BuildConfig.VERSION_CODE);
    }

    /**
     * 是否為debug
     */
    @Override
    public boolean isDebug() {
        return BuildConfig.DEBUG;
    }

    /**
     * 應用全局上下文
     */
    @Override
    public Application getApplicationContext() {
        return application;
    }
}
           

NetworkApi.java

        好了,此處進入主題了。

        介個類,是我們在發起http restful請求的核心類,一般我們在代碼裡會通過以下這樣的調用來完成okhttp3請求的發送:

LoginAPI loginAPI = NetworkApi.createService(LoginAPI.class);
           

        下面來看NetworkAPI的代碼

package com.mkyuan.aset.mall.android.network;



import com.mkyuan.aset.mall.android.network.errorhandler.ExceptionHandle;
import com.mkyuan.aset.mall.android.network.errorhandler.HttpErrorHandler;
import com.mkyuan.aset.mall.android.network.interceptor.RequestInterceptor;
import com.mkyuan.aset.mall.android.network.interceptor.ResponseInterceptor;
import com.mkyuan.aset.mall.android.util.AsetMallConstants;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * 網絡Api
 * @author mk.yuan
 * @description NetworkApi
 */
public class NetworkApi {

    /**
     * 擷取APP運作狀态及版本資訊,用于日志列印
     */
    private static INetworkRequiredInfo iNetworkRequiredInfo;
    /**
     * API通路位址
     */
    //private static final String BASE_URL = "http://192.168.0.114:9080";

    private static OkHttpClient okHttpClient;

    private static final HashMap<String, Retrofit> retrofitHashMap = new HashMap<>();

    /**
     * 初始化
     */
    public static void init(INetworkRequiredInfo networkRequiredInfo) {
        iNetworkRequiredInfo = networkRequiredInfo;
    }

    /**
     * 建立serviceClass的執行個體
     */
    public static <T> T createService(Class<T> serviceClass) {
        return getRetrofit(serviceClass).create(serviceClass);
    }

    /**
     * 配置OkHttp
     *
     * @return OkHttpClient
     */
    private static OkHttpClient getOkHttpClient() {
        //不為空則說明已經配置過了,直接傳回即可。
        if (okHttpClient == null) {
            //OkHttp建構器
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            //設定緩存大小
            int cacheSize = 100 * 1024 * 1024;
            //設定網絡請求逾時時長,這裡設定為6s
            builder.connectTimeout(6, TimeUnit.SECONDS);
            //添加請求攔截器,如果接口有請求頭的話,可以放在這個攔截器裡面
            builder.addInterceptor(new RequestInterceptor(iNetworkRequiredInfo));
            //添加傳回攔截器,可用于檢視接口的請求耗時,對于網絡優化有幫助
            builder.addInterceptor(new ResponseInterceptor());
            //當程式在debug過程中則列印資料日志,友善調試用。
            if (iNetworkRequiredInfo != null && iNetworkRequiredInfo.isDebug()) {
                //iNetworkRequiredInfo不為空且處于debug狀态下則初始化日志攔截器
                HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
                //設定要列印日志的内容等級,BODY為主要内容,還有BASIC、HEADERS、NONE。
                httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
                //将攔截器添加到OkHttp建構器中
                builder.addInterceptor(httpLoggingInterceptor);
            }
            //OkHttp配置完成
            okHttpClient = builder.build();
        }
        return okHttpClient;
    }

    /**
     * 配置Retrofit
     *
     * @param serviceClass 服務類
     * @return Retrofit
     */
    private static Retrofit getRetrofit(Class serviceClass) {
        if (retrofitHashMap.get(AsetMallConstants.ASET_MALL_ADMIN_BASE_URL + serviceClass.getName()) != null) {
            //剛才上面定義的Map中鍵是String,值是Retrofit,當鍵不為空時,必然有值,有值則直接傳回。
            return retrofitHashMap.get(AsetMallConstants.ASET_MALL_ADMIN_BASE_URL + serviceClass.getName());
        }
        //初始化Retrofit  Retrofit是對OKHttp的封裝,通常是對網絡請求做處理,也可以處理傳回資料。
        //Retrofit建構器
        Retrofit.Builder builder = new Retrofit.Builder();
        //設定通路位址
        builder.baseUrl(AsetMallConstants.ASET_MALL_ADMIN_BASE_URL);
        //設定OkHttp用戶端,傳入上面寫好的方法即可獲得配置後的OkHttp用戶端。
        builder.client(getOkHttpClient());
        //設定資料解析器 會自動把請求傳回的結果(json字元串)通過Gson轉化工廠自動轉化成與其結構相符的實體Bean
        builder.addConverterFactory(GsonConverterFactory.create());
        //設定請求回調,使用RxJava 對網絡傳回進行處理
        builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
        //retrofit配置完成
        Retrofit retrofit = builder.build();
        //放入Map中
        retrofitHashMap.put(AsetMallConstants.ASET_MALL_ADMIN_BASE_URL + serviceClass.getName(), retrofit);
        //最後傳回即可
        return retrofit;
    }

    /**
     * 配置RxJava 完成線程的切換
     *
     * @param observer 這個observer要注意不要使用lifecycle中的Observer
     * @param <T>      泛型
     * @return Observable
     */
    public static <T> ObservableTransformer<T, T> applySchedulers(final Observer<T> observer) {
        return upstream -> {
            Observable<T> observable = upstream
                    .subscribeOn(Schedulers.io())//線程訂閱
                    .observeOn(AndroidSchedulers.mainThread())//觀察Android主線程
                    .map(NetworkApi.getAppErrorHandler())//判斷有沒有500的錯誤,有則進入getAppErrorHandler
                    .onErrorResumeNext(new HttpErrorHandler<>());//判斷有沒有400的錯誤
            //訂閱觀察者
            observable.subscribe(observer);
            return observable;
        };
    }

    /**
     * 錯誤碼處理
     */
    protected static <T> Function<T, T> getAppErrorHandler() {
        return response -> {
            //當response傳回出現500之類的錯誤時
            if (response instanceof BaseResponse && ((BaseResponse) response).responseCode >= 500) {
                //通過這個異常處理,得到使用者可以知道的原因
                ExceptionHandle.ServerException exception = new ExceptionHandle.ServerException();
                exception.code = ((BaseResponse) response).responseCode;
                exception.message = ((BaseResponse) response).responseError != null ? ((BaseResponse) response).responseError : "";
                throw exception;
            }
            return response;
        };
    }
}

           

        各位可以看到,到這個類為止,retrofit、okhttp3、rxjava全部竄起來用了。

        下面這一句可以明顯看到retrofit對okhttp3是如何封裝的

//設定OkHttp用戶端,傳入上面寫好的方法即可獲得配置後的OkHttp用戶端。
        builder.client(getOkHttpClient());
           

        下面我們就要來講一下,這套封裝在實際項目中應該怎麼使用了。

在真實案例中使用retrofit2+okhttp3+rxjava

        說,有這麼一個需求:我要在每個“需要是否登入校驗的界面打開時校驗使用者是否登入,使用者是否登入是通過使用者的http請求頭-header是否帶有ut字段,且這個ut字段在背景是否存在且有效來判斷的”。

        是以,背景會提供一個post請求給到前台,如下:

@POST("/api/account/checkLoginUT")
    Observable <LoginResponseBean> checkLoginUT(@Header("ut") String ut);
           

        是以,我們在我們的業務子產品裡就定義一個API,如:LoginAPI,代碼如下:

package com.mkyuan.aset.mall.android.login.api;

import com.mkyuan.aset.mall.android.login.model.LoginResponseBean;

import io.reactivex.Observable;
import okhttp3.RequestBody;
import retrofit2.http.Body;
import retrofit2.http.Header;
import retrofit2.http.POST;

public interface LoginAPI {
    @POST("/api/account/login")
    Observable<LoginResponseBean> login(@Body RequestBody body);

    @POST("/api/account/getSmsVerifiedCode")
    Observable <LoginResponseBean> getSmsVerifiedCode(@Body RequestBody body);

    @POST("/api/account/loginBySMSVerifiedCode")
    Observable <LoginResponseBean> loginBySMSVerifiedCode(@Body RequestBody body);

    @POST("/api/account/logout")
    Observable <LoginResponseBean> logoutByUt(@Body RequestBody body);

    @POST("/api/account/checkLoginUT")
    Observable <LoginResponseBean> checkLoginUT(@Header("ut") String ut);
}
           

        我這邊的例子裡有post、有get、有header,對于retrofit來說一切隻是“方法中的參數”,唯一的差別是annotation的不同。友善吧!

        然後我們來看如何使用retrofit+okhttp3+rxjava來調用和處理一個http請求的傳回吧。

實際調用例子

        由于這是一個異步的過程,是以我們不能直接在onSuccess和onFailure裡做Android裡的界面操作,因為是異步請求和傳回的,是以你用代碼語句的順序不代表實際網絡請求值到達的時序,是以,我們這邊使用了一個小技巧。

        先定義了一個LoginCheckCallBack的接口

LoginCheckCallBack.java

package com.mkyuan.aset.mall.android.util.aop.login;

public interface LoginCheckCallBack {
    public void changeValue(boolean loginResult);
}
           

把它跟着調用的上遊,傳入調用retrofit+okhttp3+rxjava

//判斷目前是否已經登入

        LoginManager.isLogin(new LoginCheckCallBack() {
            @Override
            public void changeValue(boolean loginResult) {
                if(loginResult) {
                    Log.i(TAG, ">>>>>>使用者己登入走入下一步");
                    try {
                        //因為使用者被判定登入,是以繼續打開相應的隻有登入使用者才能看到的界面
                    } catch (Throwable e) {
                        Log.e(TAG,e.getMessage(),e);
                    }
                }else{
                    //如果未登入,去登入頁面
                    Log.i(TAG, ">>>>>>使用者未登入去登入頁面");
                    LoginManager.gotoLoginPage();
                }
            }
        });
           

        好,接着我們來看isLogin方法

public static void isLogin(LoginCheckCallBack loginCheckCallBack) {
        Context ctx = null;
        try {
            ctx = ContextUtils.getCurApplicationContext();
            SharedPreferenceHelper spHelper = new SharedPreferenceHelper(ctx);
            Map<String, String> data = spHelper.read();
            if (data.get("ut") != null && data.get("ut").trim().length() > 0) {
                String utValue = data.get("ut");
                //開始調用okhttp3, retrofit, rxjava架構
                LoginAPI loginAPI = NetworkApi.createService(LoginAPI.class);
                //loginAPI.checkLoginUT().
                loginAPI.checkLoginUT(utValue).compose(NetworkApi.applySchedulers(new BaseObserver<LoginResponseBean>() {
                    @Override
                    public void onSuccess(LoginResponseBean loginResponseBean) {
                        Log.i(TAG, ">>>>>>" + new Gson().toJson(loginResponseBean));
                        int returnCode = loginResponseBean.getCode();
                        String returnMsg = loginResponseBean.getMessage();
                        if (returnCode == 0) {
                            //result = true;
                            loginCheckCallBack.changeValue(true);
                            Log.i(TAG,
                                    ">>>>>>get verifiedCode->" + loginResponseBean.getData());
                            //startActivity(new Intent(SmsLoginActivity.this, MainActivity
                            // .class));
                        } else {
                            loginCheckCallBack.changeValue(false);
                        }
                    }

                    @Override
                    public void onFailure(Throwable e) {
                        Log.e(TAG, ">>>>>>Network Error: " + e.toString(), e);
                        loginCheckCallBack.changeValue(false);
                    }
                }));
            } else {
                loginCheckCallBack.changeValue(false);
            }
        } catch (Exception e) {
            Log.e(TAG, ">>>>>>isLogin error: " + e.getMessage());
            loginCheckCallBack.changeValue(false);
        }
    }
           

        這個案例運作後在控制台會産生以下日志,通過日志我們可以清晰的看到okhttp3請求封包和響應的全過程。

把封包請求post出去

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

得到剛才請求的傳回

Android入門第65天-mvvm模式下的retrofit2+okhttp3+rxjava開篇為什麼不能直接一個OKHTTP3解決而要用3樣東西去取待原來一樣東西就可以實作的事搭建retrofit2+okhttp3+rxjava在真實案例中使用retrofit2+okhttp3+rxjava

好了,到此結束今天的教程。

        這篇教程就是為下一篇做的前置鋪墊,因為在下一篇裡我們要使用這個isLogin方法來示範Android裡如何使用AOP來對那些需要判定是否登入的界面做統一攔截。

        最後還是那句話,自己不妨動動手試一下吧。