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架構概覽
