在上一篇文章中,简单的介绍了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, "╚═══════════════════════════════════════════════════════════════════════════════════════");
}
}
}
在通过网络请求返回数据时,先进行一个数据解析,得到结果码和错误信息,在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);
}
在实际的网络请求中有很多的异常信息和错误码,需要对这些信息要处理,在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;
}
}
网络请求中拦截器的作用是比较大的,这里我们只做日志的打印。网络访问分为请求和返回两个部分,那么就对应两个拦截器。在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;
}
}
前面的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 泛型
* @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。
在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;
}
}
}
在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中的,不要导错了。
首先在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,代码如下:
<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));
});
}
}
好了,进入下一步,这里需要对项目进行配置了
涉及到网络时,需要注意一点,就是在Android8.0之上的版本都默认使用Https访问了,需要要允许Http访问的话,需要进行一次配置。
首先在res下新建一个xml文件夹,文件夹下新建一个network_config.xml,里面的代码如下:
<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。
下面就是需要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,代码如下:
<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