前言
Retrofit是目前使用非常广泛的网络请求框架,它基于OkHttp实现,通过注解配置需求,使用简单方便,而且提供了RxJava支持,本篇文章将介绍Retrofit的一些简单用法,以及和RxJava配合使用来实现网络请求的过程。
导入依赖
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
//RxJava
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
配置Retrofit
Retrofit的创建过程如下
val builder = Retrofit.Builder()
.baseUrl(baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(factory)
下面简单说明一下Retrofit.Builder的几个方法:
baseUrl:配置请求的URL;
addCallAdapterFactory:添加CallAdapter,常用的如DefaultCallAdapterFactory支持Call类型的调用方式,而RxJava2CallAdapterFactory支持Observable类型的调用方式,也就是支持RxJava调用;
addConverterFactory:添加数据类型转换器,将后台返回的数据直接转换成实体对象,比如常用的GsonConverterFactory.create()就是用来支持gson解析,当然,我们也可以自定义转换器将返回的数据重新组装,转换成指定的实体类;
client:配置OkHttpClient,可以看到Retrofit的底层实现就是OkHttp,配置OkHttpClient时可以设置接口超时时长,管理cookie,配置缓存,添加拦截器等等;
addInterceptor:添加拦截器,拦截器的用处非常广泛,比如将后台返回的数据转换成实体类也可以通过拦截器来实现,拦截器还可以用来缓存get请求结果,打印后台返回日志的,后面将介绍几个实用的自定义拦截器;
完整代码:
class RetrofitFactory {
companion object {
//单例对象
val instance = SingleTonHolder.holder
}
//静态内部类单例模式
private object SingleTonHolder {
val holder = RetrofitFactory()
}
fun <T> createService(
clazz: Class<T>,
baseUrl: String,
factory: Converter.Factory = GsonConverterFactory.create(),
interceptor: Interceptor? = null,
timeout: Long = 30L, //默认超时时长
cache: Boolean = false //是否进行缓存
): T {
val builder = Retrofit.Builder()
.baseUrl(baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(factory)
if (interceptor != null) {
builder.client(getOkHttpBuilder(timeout, cache).addInterceptor(interceptor).build())
} else {
builder.client(getOkHttpBuilder(timeout, cache).build())
}
return builder.build().create(clazz)
}
/**
* 设置OkHttpClient.Builder
*
* @param timeout 接口超时时长
* @param cache 是否进行缓存
*/
private fun getOkHttpBuilder(timeout: Long, cache: Boolean): OkHttpClient.Builder {
//添加一个log拦截器,打印所有的log
// val httpLoggingInterceptor = HttpLoggingInterceptor()
val httpLoggingInterceptor = HttpLoggingInterceptor(HttpLogInterceptor())
//可以设置请求过滤的水平,body,basic,headers
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val builder = OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
// .retryOnConnectionFailure(true) //设置出现错误进行重新连接
.cookieJar(CookieJarManage.instance) //cookie持久化
if (cache) {
val cacheDir = File(BaseApplication.instance.cacheDir, "httpCache") //设置缓存路径
val cacheSize: Long = 10 * 1024 * 1024 //设置缓存大小为10M
builder.cache(Cache(cacheDir, cacheSize))
.addInterceptor(HttpCacheInterceptor())
}
if (Config.DEBUG) {
builder.sslSocketFactory(SSLSocketFactoryUtil.getPassAnySSLSocketFactory()) //不校验证书
.hostnameVerifier { p0, p1 -> true } //不校验服务器返回的信息
.addInterceptor(httpLoggingInterceptor) //打印日志,以便调试
}
return builder
}
//清除缓存
fun clearCache(): Boolean {
val cacheDir = File(BaseApplication.instance.cacheDir, "httpCache")
return FileUtil.deleteDirectory(cacheDir)
}
}
OkHttpClient的几个方法:
cookieJar:管理cookie
class CookieJarManage : CookieJar {
companion object {
//单例对象
val instance = SingleTonHolder.holder
}
//静态内部类单例模式
private object SingleTonHolder {
val holder = CookieJarManage()
}
private val cookieStore = HashMap<String, MutableList<Cookie>>()
//网路访问后将服务器返回的cookies和对应的url存储在cookieStore中
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
cookieStore.put(url.host(), cookies)
}
//网路访问开始的时候,根据访问的url去查找cookie,然后将cokies放到请求头里面
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> =
cookieStore.get(url.host()) ?: mutableListOf<Cookie>() //cookieStore.get(url)为null时返回mutableListOf<Cookie>()
}
sslSocketFactory:校验证书,这里选择不校验证书,代码如下:
object SSLSocketFactoryUtil {
fun getPassAnySSLSocketFactory(): SSLSocketFactory {
val sslContext = SSLContext.getInstance("TLS")
return sslContext.apply {
init(null, arrayOf<TrustManager>(TrustAllManager()), SecureRandom())
}.socketFactory
}
private class TrustAllManager : X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
}
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray<X509Certificate>()
}
}
}
addInterceptor:添加拦截器,上面的诸如HttpLogInterceptor、HttpCacheInterceptor等自定义拦截器将在下面介绍。
自定义拦截器
日志拦截器HttpLogInterceptor
构建HttpLoggingInterceptor传入自定义拦截器,自定义服务器请求报文和响应报文输出格式,比如将输出日志json格式化,代码如下
class HttpLogInterceptor : HttpLoggingInterceptor.Logger {
companion object {
const val TAG = "HttpLogger"
}
private val mMessage = StringBuilder()
override fun log(message: String) {
var message = message
// 请求或者响应开始
if (message.startsWith("--> POST")) {
mMessage.setLength(0)
}
// 以{}或者[]形式的说明是响应结果的json数据,需要进行格式化
if (message.startsWith("{") && message.endsWith("}")
|| message.startsWith("[") && message.endsWith("]")
) {
message = formatJson(message)
}
mMessage.append(message)
mMessage.append("\n")
// 响应结束,打印整条日志
if (message.startsWith("<-- END HTTP")) {
val spacingFlag =
"=============================================================================================\n"
val content = " \n$spacingFlag$mMessage$spacingFlag"
printLog(TAG, content)
}
}
private fun printLog(tag: String, msg: String) { //信息太长,分段打印
var msg = msg
//因为String的length是字符数量不是字节数量所以为了防止中文字符过多,
//把4*1024的MAX字节打印长度改为2001字符数
val max_str_length = 2001 - tag.length
//大于4000时
while (msg.length > max_str_length) {
Log.i(tag, msg.substring(0, max_str_length))
msg = msg.substring(max_str_length)
}
//剩余部分
Log.i(tag, msg)
}
//格式化Json字符串
private fun formatJson(strJson: String): String {
// 计数tab的个数
var tabNum = 0
val jsonFormat = StringBuilder()
val length = strJson.length
var last: Char = 0.toChar()
for (i in 0 until length) {
val c = strJson[i]
if (c == '{') {
tabNum++
jsonFormat.append(c + "\n")
jsonFormat.append(getSpaceOrTab(tabNum))
} else if (c == '}') {
tabNum--
jsonFormat.append("\n")
jsonFormat.append(getSpaceOrTab(tabNum))
jsonFormat.append(c)
} else if (c == ',') {
jsonFormat.append(c + "\n")
jsonFormat.append(getSpaceOrTab(tabNum))
} else if (c == ':') {
jsonFormat.append("$c ")
} else if (c == '[') {
tabNum++
val next = strJson[i + 1]
if (next == ']') {
jsonFormat.append(c)
} else {
jsonFormat.append(c + "\n")
jsonFormat.append(getSpaceOrTab(tabNum))
}
} else if (c == ']') {
tabNum--
if (last == '[') {
jsonFormat.append(c)
} else {
jsonFormat.append("\n" + getSpaceOrTab(tabNum) + c)
}
} else {
jsonFormat.append(c)
}
last = c
}
return jsonFormat.toString()
}
private fun getSpaceOrTab(tabNum: Int): String {
val sbTab = StringBuilder()
for (i in 0 until tabNum) {
sbTab.append('\t')
}
return sbTab.toString()
}
}
现在控制台输出日志格式如下,相对来说要清晰醒目的多
=============================================================================================
--> GET http://wthrcdn.etouch.cn/weather_mini?city=%E5%8C%97%E4%BA%AC
--> END GET
<-- 200 OK http://wthrcdn.etouch.cn/weather_mini?city=%E5%8C%97%E4%BA%AC (119ms)
Server: Tengine/2.3.2
Date: Sat, 13 Nov 2021 12:39:34 GMT
Connection: keep-alive
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Origin: *
Cache-Control: must-revalidate, max-age=300
Age: 0
X-Via-Ucdn: HIT by 36.158.229.109, HIT by 180.97.190.59
{
"data": {
"yesterday": {
"date": "12日星期五",
"high": "高温 15℃",
"fx": "西北风",
"low": "低温 0℃",
"fl": "<![
CDATA[
2级
]
]>",
"type": "晴"
},
"city": "北京",
"forecast": [
{
"date": "13日星期六",
"high": "高温 18℃",
"fengli": "<![
CDATA[
2级
]
]>",
"low": "低温 1℃",
"fengxiang": "西北风",
"type": "晴"
},
{
"date": "14日星期天",
"high": "高温 17℃",
"fengli": "<![
CDATA[
2级
]
]>",
"low": "低温 2℃",
"fengxiang": "西北风",
"type": "晴"
},
{
"date": "15日星期一",
"high": "高温 15℃",
"fengli": "<![
CDATA[
1级
]
]>",
"low": "低温 -1℃",
"fengxiang": "东北风",
"type": "晴"
},
{
"date": "16日星期二",
"high": "高温 16℃",
"fengli": "<![
CDATA[
1级
]
]>",
"low": "低温 -1℃",
"fengxiang": "西南风",
"type": "晴"
},
{
"date": "17日星期三",
"high": "高温 11℃",
"fengli": "<![
CDATA[
1级
]
]>",
"low": "低温 0℃",
"fengxiang": "西风",
"type": "多云"
}
],
"ganmao": "感冒易发期,外出请适当调整衣物,注意补充水分。",
"wendu": "14"
},
"status": 1000,
"desc": "OK"
}
<-- END HTTP (960-byte body)
=============================================================================================
网络缓存拦截器HttpCacheInterceptor
如果希望能够缓存接口请求结果,使得在无网络连接的情况下请求接口地址也能返回数据,也可以通过自定义拦截器来实现,原理就是使用maxAge设置在线缓存,maxStale设置离线缓存,需要注意的是这种方式只支持缓存GET请求结果,并不支持POST请求方式,代码如下:
class HttpCacheInterceptor(
private val maxAge: Int = 0, //缓存过期时间,单位是秒,默认不使用缓存
private val maxStale: Int = 3 * 24 * 60 * 60 //缓存过期时间,在请求头设置有效,在响应头设置无效,默认缓存3天
) : Interceptor {
companion object {
private const val TAG = "HttpCacheInterceptor"
}
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val isConnected = NetworkUtil.isConnected()
LogUtil.w(TAG, "网络是否连接:${isConnected}")
if (!isConnected) { //无网络的情况
LogUtil.w(TAG, "从缓存获取数据,max-stale=$maxStale")
val control = CacheControl.Builder()
.onlyIfCached()
.maxStale(maxStale, TimeUnit.SECONDS)
.build()
request = request.newBuilder()
.cacheControl(control) //从缓存读取
.build()
}
var response = chain.proceed(request)
if (isConnected) { //有网络的情况
LogUtil.w(TAG, "max-age=$maxAge")
response = response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + maxAge) //如果想要不缓存,maxAge直接设置为0
.build()
}
return response
}
}
上面的拦截器默认有网络时不使用缓存,也就是说有网络情况下每次请求都是实时请求的结果,无网络时使用缓存,缓存过期时间为3天。如果需要在有网络时也使用缓存避免频繁的接口请求,可设置maxAge不为0,如设置为5则意味着缓存过期时间为5秒。
限制最多重试次数的拦截器MaxRetryInterceptor
限制最多请求接口次数,代码如下:
class MaxRetryInterceptor(private val maxRetryCount: Int) : Interceptor {
companion object {
private const val TAG = "MaxRetryInterceptor"
}
private var retryCount = 1
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
LogUtil.w(TAG, "retry count:$retryCount")
var response = chain.proceed(request) //默认请求1次
while (!response.isSuccessful && retryCount < maxRetryCount) {
retryCount++
LogUtil.w(TAG, "retry count:$retryCount")
response = chain.proceed(request) //重试
}
return response
}
}
使用
GET请求
进行GET请求的参数类注解有@Query @QueryMap @QueryName等,下面使用一个天气接口进行GET请求:
--> GET http://wthrcdn.etouch.cn/weather_mini?city=北京
--> END GET
@Query
定义接口地址:
//获取天气信息,@Query,GET请求
@GET("weather_mini")
fun getWeatherByQuery(@Query("city") city: String): Observable<WeatherBean>
配置参数及使用:
//获取天气信息,@Query,GET请求
private fun getWeatherByQuery(city: String) {
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.WEATHER_URL)
.getWeatherByQuery(city)
.compose(SchedulerUtil.ioToMain())
.subscribe(WeatherObserver())
}
相关类介绍,实体类定义如下:
data class WeatherBean(
val status: Int,
val desc: String,
val data: DataBean?
) : Serializable {
data class DataBean(
val yesterday: YesterdayBean,
val city: String,
val forecast: List<ForecastBean>,
val ganmao: String,
val wendu: String
) : Serializable {
data class YesterdayBean(
val date: String,
val high: String,
val fx: String,
val low: String,
val fl: String,
val type: String
) : Serializable
data class ForecastBean(
val date: String,
val high: String,
val fengli: String,
val low: String,
val fengxiang: String,
val type: String
) : Serializable
}
fun isSuccess(): Boolean = status == 1000
}
UrlConstant定义如下:
object UrlConstant {
const val BAIDU_URL = "https://www.baidu.com/"
const val WEATHER_URL = "http://wthrcdn.etouch.cn/"
const val NEWS_URL = "https://api.apiopen.top/"
}
SchedulerUtil简化了在子线程请求网络,然后切换到主线程执行的过程:
object SchedulerUtil {
fun <T> ioToMain(): ObservableTransformer<T, T> {
return ObservableTransformer { observable ->
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
}
}
再看一下WeatherObserver,它返回了一个Observer,即RxJava中的观察者:
private fun WeatherObserver(): Observer<WeatherBean> {
return object : Observer<WeatherBean> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(bean: WeatherBean) {
if (bean.isSuccess()) {
val result = JsonUtil.formatJson(Gson().toJson(bean))
alert(result)
} else {
showToast(bean.desc)
}
}
override fun onError(e: Throwable) {
showToast(ExceptionUtil.convertExceptopn(e))
e.printStackTrace()
}
}
}
@QueryMap
定义接口地址:
//获取天气信息,@QueryMap,GET请求
@GET("weather_mini")
fun getWeatherByQueryMap(@QueryMap map: HashMap<String, Any>): Observable<WeatherBean>
配置参数及使用:
//获取天气信息,@QueryMap,GET请求
private fun getWeatherByQueryMap(city: String) {
val map = HashMap<String, Any>()
map.put("city", city)
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.WEATHER_URL)
.getWeatherByQueryMap(map)
.compose(SchedulerUtil.ioToMain())
.subscribe(WeatherObserver())
}
可以看到,QueryMap可以将多个Query参数直接封装成一个HashMap传入,但是最后结果和Query是一样的。
无Query的情况
比如只是简单的访问https://www.baidu.com/
定义接口:
//简单的访问IP地址,Retrofit + RxJava
@GET("/")
fun accessUrlRxJava(): Observable<ResponseBody>
调用,传入地址即可:
//访问网址,Retrofit + RxJava
private fun accessUrlRxJava(url: String) {
RetrofitFactory.instance.createService(ApiService::class.java, url)
.accessUrlRxJava()
.compose(SchedulerUtil.ioToMain())
//.subscribeOn(Schedulers.io())
//.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ResponseBody> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(response: ResponseBody) {
showToast("访问成功!")
alert(response.string())
}
override fun onError(e: Throwable) {
showToast(ExceptionUtil.convertExceptopn(e))
e.printStackTrace()
}
})
}
POST请求
进行POST请求的参数类注解有@Field @FieldMap @Body等,以请求网易新闻列表为例:
--> POST https://api.apiopen.top/getWangYiNews
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
count=2&page=1
--> END POST (14-byte body)
@Field
定义接口地址:
//获取网易新闻,@Field,POST请求
@FormUrlEncoded
@POST("getWangYiNews")
fun getWangYiNewsByField(@Field("page") page: String, @Field("count") count: String): Observable<NewsListBean>
配置参数及使用:
//获取网易新闻,@Field,POST请求
private fun getWangYiNewsByField(page: String, count: String) {
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.NEWS_URL)
.getWangYiNewsByField(page, count)
.compose(SchedulerUtil.ioToMain())
.subscribe(NewsObserver())
}
相关类介绍,实体类定义如下:
data class NewsListBean(
val code: Int,
val message: String,
val result: MutableList<ResultBean>?
) : Serializable {
data class ResultBean(
val path: String,
val image: String,
val title: String,
val passtime: String
) : Serializable
fun isSuccess(): Boolean = code == 200
}
NewsObserver代码如下:
private fun NewsObserver(): Observer<NewsListBean> {
return object : Observer<NewsListBean> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(bean: NewsListBean) {
if (bean.isSuccess()) {
val result = JsonUtil.formatJson(Gson().toJson(bean))
alert(result)
} else {
showToast(bean.message)
}
}
override fun onError(e: Throwable) {
showToast(ExceptionUtil.convertExceptopn(e))
e.printStackTrace()
}
}
}
@FieldMap
定义接口地址:
//获取网易新闻,@FieldMap,POST请求
@FormUrlEncoded
@POST("getWangYiNews")
fun getWangYiNewsByFieldMap(@FieldMap map: HashMap<String, Any>): Observable<NewsListBean>
配置参数及使用:
//获取网易新闻,@FieldMap,POST请求
private fun getWangYiNewsByFieldMap(page: String, count: String) {
val map = HashMap<String, Any>()
map.put("page", page)
map.put("count", count)
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.NEWS_URL)
.getWangYiNewsByFieldMap(map)
.compose(SchedulerUtil.ioToMain())
.subscribe(NewsObserver())
}
@Body
定义接口地址:
//获取网易新闻,@Body,POST请求
@POST("getWangYiNews")
fun getWangYiNewsByBody(@Body requestBody: RequestBody): Observable<NewsListBean>
配置参数及使用:
//获取网易新闻,@Body,POST请求
private fun getWangYiNewsByBody(page: String, count: String) {
val map = HashMap<String, Any>()
map.put("page", page)
map.put("count", count)
val sb = StringBuilder()
map.forEach {
sb.append("${it.key}=${it.value}&")
}
val data = sb.toString().substring(0, sb.toString().length - 1)
//val data = "page=$page&count=$count"
val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), data)
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.NEWS_URL)
.getWangYiNewsByBody(body)
.compose(SchedulerUtil.ioToMain())
.subscribe(NewsObserver())
}
@Body是以表单形式提交POST请求
缓存GET请求
//测试缓存机制
private fun testCache(city: String) {
RetrofitFactory.instance.createService(ApiService::class.java, UrlConstant.WEATHER_URL, cache = true)
.getWeatherByQuery(city)
.compose(SchedulerUtil.ioToMain())
.subscribe(WeatherObserver())
}
RetrofitFactory相关缓存设置代码:
/**
* 设置OkHttpClient.Builder
*
* @param timeout 接口超时时长
* @param cache 是否进行缓存
*/
private fun getOkHttpBuilder(timeout: Long, cache: Boolean): OkHttpClient.Builder {
//添加一个log拦截器,打印所有的log
val httpLoggingInterceptor = HttpLoggingInterceptor()
val httpLoggingInterceptor = HttpLoggingInterceptor(HttpLogInterceptor())
//可以设置请求过滤的水平,body,basic,headers
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val builder = OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) //设置出现错误进行重新连接
.cookieJar(CookieJarManage.instance) //cookie持久化
if (cache) {
val cacheDir = File(BaseApplication.instance.cacheDir, "httpCache") //设置缓存路径
val cacheSize: Long = 10 * 1024 * 1024 //设置缓存大小为10M
builder.cache(Cache(cacheDir, cacheSize))
.addInterceptor(HttpCacheInterceptor())
}
if (Config.DEBUG) {
builder.sslSocketFactory(SSLSocketFactoryUtil.getPassAnySSLSocketFactory()) //不校验证书
.hostnameVerifier { p0, p1 -> true } //不校验服务器返回的信息
.addInterceptor(httpLoggingInterceptor) //打印日志,以便调试
}
return builder
}
//清除缓存
fun clearCache(): Boolean {
val cacheDir = File(BaseApplication.instance.cacheDir, "httpCache")
return FileUtil.deleteDirectory(cacheDir)
}
设置最多尝试请求3次
//最多重试3次
private fun testMaxRetry(city: String) {
RetrofitFactory.instance.createService(
ApiService::class.java,
UrlConstant.WEATHER_URL,
interceptor = MaxRetryInterceptor(3)
).getWeatherByQuery(city)
.compose(SchedulerUtil.ioToMain())
.subscribe(WeatherObserver())
}
源码地址
不得不说,Retrofit是一个非常好用的网络请求框架,而RxJava也是非常强大的,本文只是介绍了Retrofit和RxJava一些基础的用法,还有很多实用的方法需要去系统的学习!这里就不一一介绍了。
|