当前位置: 代码迷 >> 综合 >> Android MVVM框架搭建(二)OKHttp + Retrofit + RxJava
  详细解决方案

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

热度:25   发布时间:2023-12-18 15:54:00.0

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{}闭包中增加如下依赖:

	//retrofit2implementation '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'//rxjavaimplementation 'io.reactivex.rxjava2:rxandroid:2.1.1'implementation 'io.reactivex.rxjava2:rxjava:2.2.12'//gsonimplementation '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")@Exposepublic Integer responseCode;//返回的错误信息@SerializedName("res_error")@Exposepublic String responseError;
}

然后再自定义一个BaseObserver类,继承自rxjava的Observer。依然在network包下创建,代码如下:

/*** 自定义Observer** @author llw*/
public abstract class BaseObserver<T> implements Observer<T> {
    //开始@Overridepublic void onSubscribe(Disposable disposable) {
    }//继续@Overridepublic void onNext(T t) {
    onSuccess(t);}//异常@Overridepublic void onError(Throwable e) {
    onFailure(e);}//完成@Overridepublic 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,最后也会走到这个函数来统一处理;*/@Overridepublic 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;}/*** 拦截*/@Overridepublic 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";/*** 拦截*/@Overridepublic 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;//设置网络请求超时时长,这里设置为6sbuilder.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转化工厂自动转化成与其结构相符的实体Beanbuilder.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>() {
    @Overridepublic void onSuccess(BiYingResponse biYingImgResponse) {
    KLog.d(new Gson().toJson(biYingImgResponse));biyingImage.setValue(biYingImgResponse);}@Overridepublic 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><variablename="viewModel"type="com.llw.mvvm.viewmodels.LoginViewModel" /></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"android:padding="32dp"><TextViewandroid:id="@+id/tv_account"android:text="@{viewModel.user.account}"android:layout_width="wrap_content"android:layout_height="wrap_content"/><TextViewandroid: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.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><com.google.android.material.textfield.TextInputEditTextandroid: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.TextInputLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="12dp"><com.google.android.material.textfield.TextInputEditTextandroid: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.MaterialButtonandroid: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;@Overrideprotected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);//数据绑定视图dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);loginViewModel = new LoginViewModel();//Model → ViewUser 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;}/*** 版本名*/@Overridepublic String getAppVersionName() {
    return BuildConfig.VERSION_NAME;}/*** 版本号*/@Overridepublic String getAppVersionCode() {
    return String.valueOf(BuildConfig.VERSION_CODE);}/*** 是否为debug*/@Overridepublic boolean isDebug() {
    return BuildConfig.DEBUG;}/*** 应用全局上下文*/@Overridepublic Application getApplicationContext() {
    return application;}
}

这个要在AndroidManifest.xml中做配置,不过先不着急,先在com.llw.mvvm包下创建一个BaseApplication类,里面的代码如下:

/*** 自定义 Application* @author llw*/
public class BaseApplication extends Application {
    @SuppressLint("StaticFieldLeak")public static Context context;@Overridepublic 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><variablename="viewModel"type="com.llw.mvvm.viewmodels.MainViewModel" /></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"tools:context=".MainActivity"><com.llw.mvvm.view.CustomImageViewbiyingUrl="@{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;@Overrideprotected 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 欢迎Star和Fork
CSDN:MVVMDemo_2.rar

  相关解决方案