主頁 > 移動端開發 > Android MVVM框架搭建(二)OKHttp + Retrofit + RxJava

Android MVVM框架搭建(二)OKHttp + Retrofit + RxJava

2021-11-03 08:36:37 移動端開發

Android MVVM框架搭建(二)Retrofit + RxJava

  • 前言
  • 正文
    • 一、引入依賴
    • 二、工具類
    • 三、構建網路框架
      • 1. Base
      • 2. 例外處理
      • 3. 攔截器
      • 4. 網路請求服務
    • 四、使用網路框架
      • 1. 創建回傳物體
      • 2. 創建ApiService
      • 3. 創建資料存盤
      • 4. 專案環境配置
      • 5. 必應圖片顯示
    • 五、原始碼

前言

??在上一篇文章中,簡單的介紹了MVVM框架的成員和簡單使用,一個成熟的框架自然是離不開網路訪問的,因此文本將通過Retrofit + RxJava去為MVVM框架增加一個網路請求模塊,

正文

??讓我們開始吧!說實話搭建框架首先要做的是創建一個library,但是我并沒有這么做,不是不去做,而是還不成熟,現在這個框架還不完整,還少了很多實際開發中需要的東西,因此一個成熟的框架應該是經歷過專案考驗的,此時再從這個專案中去提煉出框架得到才是精華,就好像建房子一樣,基礎模型有了,最終的樣子取決于你的裝修,這些裝修的作業里面也有通用的部分,這部分是可以放進框架里面的,所以當你打算做一個框架的時候,千萬不要著急,立足于實踐,從實踐中積累經驗,當然了你要是直接用別人寫好的框架,也能夠去解決問題,這一點也是可以的,但是會不踏實,只有自己百分百寫出來的東西,自己才能知根知底,說這些的意義是要注重實踐和思考,拿來主義并不可取,

一、引入依賴

??要知道做完GitHub上Android的最受歡迎的開源庫,Retrofit的知名度毋庸置疑,這得益于它的設計模式和使用方式,它作為OkHttp的進一步封裝無疑是很成功的,雖然底層去執行網路訪問的還是OkHttp,但是我們卻更喜歡Retrofit,下面進入使用的環節,首先要進行依賴庫的引入,

在app的build.gradle的dependencies{}閉包中增加如下依賴:

	//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'

添加位置如下圖所示:
在這里插入圖片描述

然后點擊Sync Now,進行依賴庫同步,

二、工具類

??在實際的網路請求中會需要列印日志和一些請求時間的顯示,方便排查問題,下面在com.llw.mvvm下新建一個network包,包下新建一個INetworkRequiredInfo介面,里面的代碼如下:

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

    /**
     * 獲取App版本號
     */
    String getAppVersionCode();

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

    /**
     * 獲取全域背景關系引數
     */
    Application getApplicationContext();
}

這里就是要在請求網路介面的時候列印當前的App的運行資訊,可以根據實際的需求再進行一次補充,

在network包下新建一個utils包,包下新建一個DateUtil類,代碼如下:

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

日志工具類,在utils包下新建KLog類,代碼如下:

/**
 * 自定義日志類
 */
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, "╚═══════════════════════════════════════════════════════════════════════════════════════");
        }
    }
}

三、構建網路框架

1. Base

??在通過網路請求回傳資料時,先進行一個資料決議,得到結果碼和錯誤資訊,在network包下新建一個BaseResponse類,代碼如下:

/**
 * 基礎回傳類
 * @author llw
 */
public class BaseResponse {

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

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

然后再自定義一個BaseObserver類,繼承自rxjava的Observer,依然在network包下創建,代碼如下:

/**
 * 自定義Observer
 *
 * @author llw
 */
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);
}

2. 例外處理

??在實際的網路請求中有很多的例外資訊和錯誤碼,需要對這些資訊要處理,在network包下新建一個errorhandler包,包下新建一個HttpErrorHandler類,代碼如下:

/**
 * 網路錯誤處理
 * @author llw
 */
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));
    }
}

然后再在network包下創建一個ExceptionHandle類,代碼如下:

/**
 * 例外處理
 * @author llw
 */
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;
    }
}

3. 攔截器

??網路請求中攔截器的作用是比較大的,這里我們只做日志的列印,網路訪問分為請求和回傳兩個部分,那么就對應兩個攔截器,在network包下新建一個interceptor包,包下新建一個RequestInterceptor類,代碼如下:

/**
 * 請求攔截器
 * @author llw
 */
public class RequestInterceptor implements Interceptor {
    /**
     * 網路請求資訊
     */
    private INetworkRequiredInfo iNetworkRequiredInfo;

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

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

??這里是簡單的列印了一下,app的版本號和版本名,因為實際開發中,可能有多個版本在進行測驗,這樣可以幫助快速區分,

下面是回傳攔截器,在interceptor包下新建一個ResponseInterceptor類,代碼如下:

/**
 * 回傳攔截器(回應攔截器)
 *
 * @author llw
 */
public class ResponseInterceptor implements Interceptor {

    private static final String TAG = "ResponseInterceptor";

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

4. 網路請求服務

??前面的3步操作都屬于準備環節,核心的地方在這里,也就是創建網路服務,這里會將OKHttp、Retrofit、RxJava串起來,在network包下新建一個NetworkApi類,里面的代碼如下:

/**
 * 網路Api
 * @author llw
 * @description NetworkApi
 */
public class NetworkApi {

    /**
     * 獲取APP運行狀態及版本資訊,用于日志列印
     */
    private static INetworkRequiredInfo iNetworkRequiredInfo;
    /**
     * API訪問地址
     */
    private static final String BASE_URL = "https://cn.bing.com";
    
    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(BASE_URL + serviceClass.getName()) != null) {
            //剛才上面定義的Map中鍵是String,值是Retrofit,當鍵不為空時,必然有值,有值則直接回傳,
            return retrofitHashMap.get(BASE_URL + serviceClass.getName());
        }
        //初始化Retrofit  Retrofit是對OKHttp的封裝,通常是對網路請求做處理,也可以處理回傳資料,
        //Retrofit構建器
        Retrofit.Builder builder = new Retrofit.Builder();
        //設定訪問地址
        builder.baseUrl(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(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;
        };
    }
}

網路框架就構建完成了,network包內容如下圖所示:
在這里插入圖片描述

??這個網路框架在使用前需要先進行初始化,后面有使用的實體,代碼中的注釋應該是很明白了,總的來說就是一個思路,OkHttp做底層的網路訪問,Retrofit做上層網路請求介面的封裝,同時將需要的資料決議成物體,同時Retrofit還有對RxJava的支持,這樣就可以在請求的時候做執行緒切換,切換到子執行緒,在資料回傳的時候切換到主執行緒,避免了在主執行緒中進行耗時操作的問題,因此那么多人說Retrofit強大是有原因的,因為你不會看到有人直接拿OKHttp + Rxjava進行使用而跳過Retrofit的,所以這個組合使用是有其道理在里面的,對于任何不了解的事情,都不要急著下結論,

四、使用網路框架

??網路框架搭建好了,下面也要能夠使用才行對吧,這里我通過訪問必應的每日一圖來作為演示,必應每日一圖的訪問地址如下所示:

"https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1"

不管拿到任何API介面都要先進行一次測驗,這是對自己負責,不過過于相信別人,否則你會吃虧的,
在這里插入圖片描述
通過瀏覽器訪問得到回傳結果,然后我們通過回傳的資料構建一個物體Bean,

1. 創建回傳物體

在model包下新建一個BiYingResponse類,代碼如下:

/**
 * 必應訪問介面回傳資料物體
 * @author llw
 * @description BiYingImgResponse
 */
public class BiYingResponse {

    private TooltipsBean tooltips;
    private List<ImagesBean> images;

    public TooltipsBean getTooltips() {
        return tooltips;
    }

    public void setTooltips(TooltipsBean tooltips) {
        this.tooltips = tooltips;
    }

    public List<ImagesBean> getImages() {
        return images;
    }

    public void setImages(List<ImagesBean> images) {
        this.images = images;
    }

    public static class TooltipsBean {
        private String loading;
        private String previous;
        private String next;
        private String walle;
        private String walls;

        public String getLoading() {
            return loading;
        }

        public void setLoading(String loading) {
            this.loading = loading;
        }

        public String getPrevious() {
            return previous;
        }

        public void setPrevious(String previous) {
            this.previous = previous;
        }

        public String getNext() {
            return next;
        }

        public void setNext(String next) {
            this.next = next;
        }

        public String getWalle() {
            return walle;
        }

        public void setWalle(String walle) {
            this.walle = walle;
        }

        public String getWalls() {
            return walls;
        }

        public void setWalls(String walls) {
            this.walls = walls;
        }
    }

    public static class ImagesBean {
        private String startdate;
        private String fullstartdate;
        private String enddate;
        private String url;
        private String urlbase;
        private String copyright;
        private String copyrightlink;
        private String title;
        private String quiz;
        private boolean wp;
        private String hsh;
        private int drk;
        private int top;
        private int bot;
        private List<?> hs;

        public String getStartdate() {
            return startdate;
        }

        public void setStartdate(String startdate) {
            this.startdate = startdate;
        }

        public String getFullstartdate() {
            return fullstartdate;
        }

        public void setFullstartdate(String fullstartdate) {
            this.fullstartdate = fullstartdate;
        }

        public String getEnddate() {
            return enddate;
        }

        public void setEnddate(String enddate) {
            this.enddate = enddate;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getUrlbase() {
            return urlbase;
        }

        public void setUrlbase(String urlbase) {
            this.urlbase = urlbase;
        }

        public String getCopyright() {
            return copyright;
        }

        public void setCopyright(String copyright) {
            this.copyright = copyright;
        }

        public String getCopyrightlink() {
            return copyrightlink;
        }

        public void setCopyrightlink(String copyrightlink) {
            this.copyrightlink = copyrightlink;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getQuiz() {
            return quiz;
        }

        public void setQuiz(String quiz) {
            this.quiz = quiz;
        }

        public boolean isWp() {
            return wp;
        }

        public void setWp(boolean wp) {
            this.wp = wp;
        }

        public String getHsh() {
            return hsh;
        }

        public void setHsh(String hsh) {
            this.hsh = hsh;
        }

        public int getDrk() {
            return drk;
        }

        public void setDrk(int drk) {
            this.drk = drk;
        }

        public int getTop() {
            return top;
        }

        public void setTop(int top) {
            this.top = top;
        }

        public int getBot() {
            return bot;
        }

        public void setBot(int bot) {
            this.bot = bot;
        }

        public List<?> getHs() {
            return hs;
        }

        public void setHs(List<?> hs) {
            this.hs = hs;
        }
    }
}

2. 創建ApiService

??在com.llw.mvvm包下新建一個api包,api包下新建一個ApiService類,代碼如下:

/**
 * 所有的Api網路介面
 * @author llw
 */
public interface ApiService {

    /**
     * 必應每日一圖
     */
    @GET("/HPImageArchive.aspx?format=js&idx=0&n=1")
    Observable<BiYingResponse> biying();
}

??這里的意思很明白就是,把一個完整的網路連接進行一個拆分,一部分是不變的,一部分是變化的,這也符合實際開發中的需求,一個服務器上有多個介面,這樣做在更改服務器的時候就只要更改不變的一處就可以了,這里的Observable依然是RxJava中的,不要導錯了,

3. 創建資料存盤

??首先在com.llw.mvvm包下面創建一個repository包,repository包下新建一個MainRepository類,里面的代碼如下:

/**
 * Main存盤庫 用于對資料進行處理
 * @author llw
 */
public class MainRepository {

    @SuppressLint("CheckResult")
    public MutableLiveData<BiYingResponse> getBiYing() {
        final MutableLiveData<BiYingResponse> biyingImage = new MutableLiveData<>();
        ApiService apiService = NetworkApi.createService(ApiService.class);
        apiService.biying().compose(NetworkApi.applySchedulers(new BaseObserver<BiYingResponse>() {
            @Override
            public void onSuccess(BiYingResponse biYingImgResponse) {
                KLog.d(new Gson().toJson(biYingImgResponse));
                biyingImage.setValue(biYingImgResponse);
            }

            @Override
            public void onFailure(Throwable e) {
                KLog.e("BiYing Error: " + e.toString());
            }
        }));
        return biyingImage;
    }
}

??這里就是對剛才的網路介面進行請求,然后回傳LiveData,這里為什么要單獨建一個包來管理頁面的資料獲取,其實你可以將這里的代碼寫到MainViewModel中,但是你得保證唯一性,因為假如你一個介面在多個地方會使用,你每一個都寫到對應的ViewModel中,是不是就會有很多的重復代碼?這樣就不是很好,現在這樣做雖然會麻煩一些,但是好處是很多的,因為我們現在也只是獲取網路資料,實際中App的資料還有多個來源,本地資料庫、本地快取,都是可以拿資料的,這些環節如果要寫的話,都是要寫在這個Repository中的,如果你放到ViewModel中,會導致里面的代碼量很大,因為你一個ViewModel中可能有多個網路請求,這很正常,

??本來下一步就是應該要去MainViewModel中呼叫剛才MainRepository中的方法了,但是由于之前MainViewModel中有上一篇文章的代碼,因此我們需要做一個轉移,說白了,就是新建一個LoginActivity去把MainActivity的內容都移過去,這一步我就只貼代碼了,不做說明了,因為上一篇已經說過了,

??在com.llw.mvvm包下新建一個LoginActivity,對應的布局是activity_login.xml,下面在viewmodels包下新建一個LoginViewModel類,代碼如下:

/**
 * 登錄頁面ViewModel
 * @author llw
 */
public class LoginViewModel extends ViewModel {

    public MutableLiveData<User> user;

    public MutableLiveData<User> getUser(){
        if(user == null){
            user = new MutableLiveData<>();
        }
        return user;
    }
}

然后修改activity_login.xml,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <!--系結資料-->
    <data>
        <variable
            name="viewModel"
            type="com.llw.mvvm.viewmodels.LoginViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="32dp">

        <TextView
            android:id="@+id/tv_account"
            android:text="@{viewModel.user.account}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_marginBottom="24dp"
            android:id="@+id/tv_pwd"
            android:text="@{viewModel.user.pwd}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_account"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:text="@={viewModel.user.account}"
                android:hint="賬號" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_pwd"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:text="@={viewModel.user.pwd}"
                android:hint="密碼"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_margin="24dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="登  錄"
            app:cornerRadius="12dp" />

    </LinearLayout>
</layout>

下面修改LoginActivity中的代碼

public class LoginActivity extends AppCompatActivity {

    private ActivityLoginBinding dataBinding;
    private LoginViewModel loginViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //資料系結視圖
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);

        loginViewModel = new LoginViewModel();
        //Model → View
        User user = new User("admin", "123456");
        loginViewModel.getUser().setValue(user);
        //獲取觀察物件
        MutableLiveData<User> user1 = loginViewModel.getUser();
        user1.observe(this, user2 -> dataBinding.setViewModel(loginViewModel));

        dataBinding.btnLogin.setOnClickListener(v -> {
            if (loginViewModel.user.getValue().getAccount().isEmpty()) {
                Toast.makeText(LoginActivity.this, "請輸入賬號", Toast.LENGTH_SHORT).show();
                return;
            }
            if (loginViewModel.user.getValue().getPwd().isEmpty()) {
                Toast.makeText(LoginActivity.this, "請輸入密碼", Toast.LENGTH_SHORT).show();
                return;
            }
            Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
            startActivity(new Intent(LoginActivity.this,MainActivity.class));
        });
    }
}

好了,進入下一步,這里需要對專案進行配置了

4. 專案環境配置

??涉及到網路時,需要注意一點,就是在Android8.0之上的版本都默認使用Https訪問了,需要要允許Http訪問的話,需要進行一次配置,

首先在res下新建一個xml檔案夾,檔案夾下新建一個network_config.xml,里面的代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

下面要創建一個實作類,實作network包中的INetworkRequiredInfo介面,在com.llw.mvvm包下新建一個NetworkRequiredInfo

/**
 * 網路訪問資訊
 * @author llw
 */
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;
    }
}

這個要在AndroidManifest.xml中做配置,不過先不著急,先在com.llw.mvvm包下創建一個BaseApplication類,里面的代碼如下:

/**
 * 自定義 Application
 * @author llw
 */
public class BaseApplication extends Application {

    @SuppressLint("StaticFieldLeak")
    public static Context context;

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化
        NetworkApi.init(new NetworkRequiredInfo(this));
        context = getApplicationContext();
    }

    public static Context getContext() {
        return context;
    }
}

然后我們去AndroidManifest.xml中進行配置,配置如下圖所示:
在這里插入圖片描述
第一個:網路請求是需要靜態權限的,
第二個:配置我們剛才自定義的BaseApplication,在onCreate中對網路框架進行了初始化,如果不配置,使用的就是系統的Application,
第三個:配置HTTP網路訪問許可,
第四個:就是修改LoginActivity作為第一個啟動的Activity,當點擊登錄按鈕是就會進入到MainActivity,

5. 必應圖片顯示

下面就是需要MainViewModel的代碼,如下:

/**
 * 主頁面ViewModel
 *
 * @author llw
 * @description MainViewModel
 */
public class MainViewModel extends ViewModel {

    public LiveData<BiYingResponse> biying;

    public void getBiying(){
        biying = new MainRepository().getBiYing();
    }
}

由于是加載網路圖片,這里使用Glide框架進行加載,在app的build.gradle中中dependencies{}閉包下增加如下依賴:

	//圖片加載框架
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

在這里插入圖片描述
然后Sync Now同步一下即可,

??下面就是顯示圖片了,這里要思考一個問題,那就是圖片能不能通過DataBinding的方式進行資料系結,是可以的,不過需要我們自定義一個ImageView,用于系結網路地址,很簡單的一個View,在com.llw.mvvm下新建一個view包,包下新建一個CustomImageView,代碼如下:

/**
 * 自定義View
 * @author llw
 * @description CustomImageVIew
 */
public class CustomImageView extends AppCompatImageView {

    public CustomImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     * 必應壁紙  因為拿到的url不完整,因此需要做一次地址拼接
     * @param imageView 圖片視圖
     * @param url 網路url
     */
    @BindingAdapter(value = {"biyingUrl"}, requireAll = false)
    public static void setBiyingUrl(ImageView imageView, String url) {
        String assembleUrl = "http://cn.bing.com" + url;
        KLog.d(assembleUrl);
        Glide.with(BaseApplication.getContext()).load(assembleUrl).into(imageView);
    }

    /**
     * 普通網路地址圖片
     * @param imageView 圖片視圖
     * @param url 網路url
     */
    @BindingAdapter(value = {"networkUrl"}, requireAll = false)
    public static void setNetworkUrl(ImageView imageView, String url) {
        Glide.with(BaseApplication.getContext()).load(url).into(imageView);
    }
}

然后修改activity_main.xml,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <!--系結資料-->
    <data>
        <variable
            name="viewModel"
            type="com.llw.mvvm.viewmodels.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <com.llw.mvvm.view.CustomImageView
            biyingUrl="@{viewModel.biying.images.get(0).url}"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
</layout>

布局中主要的內容就是

biyingUrl="@{viewModel.biying.images.get(0).url}"

這里我們剛才在自定義View中寫好的一個方法,通過注解運行編譯時技術參考的,

下面就是MainActivity中的代碼了,如下所示:

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding dataBinding;
    private MainViewModel mainViewModel;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //資料系結視圖
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
        //網路請求
        mainViewModel.getBiying();
        //回傳資料時更新ViewModel,ViewModel更新則xml更新
        mainViewModel.biying.observe(this, biYingImgResponse -> dataBinding.setViewModel(mainViewModel));
    }
}

這里的代碼在上一篇文章中都有說過,所以很簡單也很好理解,

下面運行一下,看看效果
在這里插入圖片描述
很好這就加載出來了,這說明我們的網路框架沒有啥問題,而且圖片系結也沒有問題,下面我們來看看日志吧,
在這里插入圖片描述
首先是請求攔截器,這里列印了版本號、版本名、請求時間,
在這里插入圖片描述
這里顯示的是回傳攔截器中對這個API請求所花費的時間,333ms,
在這里插入圖片描述
??其實OkHttp的花費耗時更準確,只用了329ms,相差4ms,因為我們現在是組合使用,因此還可以,4ms的效果不算什么,同時再看這個KLog工具類是可以列印出寫日志的類名和行數的,不知道你注意到沒有,

好了,本篇文章就到這里,

五、原始碼

GitHub:MVVM-Demo

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/345793.html

標籤:其他

上一篇:Flutter架構概覽

下一篇:大廠都在用的UI框架,《Android高級UI開源框架進階解密》,值得收藏的高級UI開源框架總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more