第四课 Retrofit 网络封装
项目源码地址
https://github.com/liaozhoubei/NetEasyNews/tree/dev_kotlin
现在 android 流行的网络框架大致为三种 Volley/okhttp/retrofit.
其中 Volley 为谷歌出品,是一款小巧方便的网络请求框架,但是用过之后感觉如果需要进行定制化开发还是有点困难的。
Okhttp/retrofit 为square 出品,是现在流行幅度比较大的网络框架,其中 retrofit 是对 okhttp 的进一步封装。
当然 okhttp 调用起来较为繁琐,一般需要开发者进行二次封装才能更合理的使用。
retrofit 则是使用了注解技术,使用较为方便,但是如果想深入了解,还是有点难度的。
另一方面,retrofit 可以跟 okhttp 合起来使用,进行深层次的定制化,现在这个项目就是使用 retrofit + okhttp 这种方式
引入 retrofit 与 OKhttp 相关类包
首先在 versions.gradle 文件中添加相关类包,在第一课时对这个文件讲解过,起着统一项目各个 module 所要使用的类包,导入类包如下:
def retrofit = [:]
retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
retrofit.simplexml = "com.squareup.retrofit2:converter-simplexml:$versions.retrofit"
retrofit.scalars = "com.squareup.retrofit2:converter-scalars:$versions.retrofit"
deps.retrofit = retrofit
deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
其中
converter-gson :retrofit 使用它直接解析 json 格式数据
converter-simplexml : retrofit 使用它直接解析 xml 数据
converter-scalars : retrofit 使用它解析网络数据为 string
okhttp3:logging-interceptor: okhttp 的拦截器
从导入的类包中我们就能看到 retrofit 的强大功能,它支持直接对 json 格式和 xml 格式进行直接解析,不需要我们在做多一步开发,这是多好的事情啊!
题外话:retrofit 解析 xml 数据中 converter-simplexml 被标注为废弃,然而它的替代方案 converter-jaxb 并不支持 android
在 versions.gradle 设置好类包后,需要在 app 这个 module 下的
build.gradle 中正式添加依赖,如下:
dependencies {
....
implementation deps.okhttp3
implementation deps.retrofit.runtime
implementation deps.retrofit.gson
implementation deps.retrofit.simplexml
implementation deps.retrofit.scalars
....
}
这样就正式添加好 retrofit 的相关依赖了。
retrofit + Okhttp 的使用
retrofit + okhttp 的配合使用非常简单,因为 retrofit 本身就是使用 okhttp 来进行网络请求的,因此它有方法可以直接将自定义好的 okhttp 设置进去,然后直接使用,代码如下:
fun getInstance(baseUrl: HttpUrl, context: Context): NetEasyService {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(getOkHttpClient(context))
.addConverterFactory(GsonConverterFactory.create())
.build()
val netEasyService = retrofit.create(NetEasyService::class.java)
return netEasyService
}
fun getOkHttpClient(context: Context): OkHttpClient {
var okHttpClient = OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.sslSocketFactory(createSSLSocketFactory()!!, TrustAllManager()) // 测试版本,信任所有未知来源证书
.build()
return okHttpClient
}
如此便设置好了 Retrofit ,然后直接进行调用即可。其中 baseUrl 是网络接口调用的基本url,说直白点就是 域名+端口号,如 http://192.168.1.2:8080/ 这种地址。
GsonConverterFactory.create() 则是接口数据回来后要对其怎样解析,这里设置默认使用 gson 解析
但是其中有 NetEasyService::class.java ,这个是用于设置需要调用的接口类。
设置 retrofit 接口类
retrofit 的接口类是否则相关的接口请求的类,通过这个类,完全简化了接口请求的各种参数配置,剩下的只需要对结果进行判断就好了,代码如下:
interface NetEasyService {
@GET("nc/article/list/{tid}/{startIndex}-20.html" )
fun getNewsListNormalBean(@Path("tid") tid: String, @Path("startIndex") startIndex: Int): Call<NewsBean>
@GET(Api.SpecialColumn2)
fun getVideoBean(@Query("channel") channelId:String = "T1457068979049",
@Query("subtab") Video_Recom:String ="Video_Recom",
@Query("size") size:Int =10,
@Query("offset") startIndex: Int,
@Query("devId") devId: String="44t6%2B5mG3ACAOlQOCLuIHg%3D%3D"
): Call<VideoBean>
}
retrofit 中存在很多注解,这里使用到的 @GET 代表使用 get 请求网络,@Path 则是在接口中需要传入的参数。其中要注意,如果请求的接口中存在 key-value 这样需要传入的参数需要使用 @Query 注解,接口类似于这样的:
http://192.168.1.2/getMessage?name=ken
这种类型的接口要只用 @Query ,否则会报错。
写完接口,那就是调用了,调用方法如下:
var retrofitHelper = RetrofitHelper.getInstance(HttpUrl.parse(Api.host)!!, context);
var call = retrofitHelper.getNewsListNormalBean("T1467284926140", 0);
var reponse = call.execute();// 同步调用
var body: NewsBean? = reponse.body();
var list = body?.getNewslist()
是否调用起来非常简单,只需要简单的调用,然后就直接把 json 数据转换为对象了!
okhttp拦截器
我们导包的时候导入了 com.squareup.okhttp3:logging-interceptor 这个包,这个包是 okhttp 的拦截器。这是一个很有用的工具。
Retrofit 有个小缺点,就是所有的东西都封装了起来,甚至连 url 都封装起来了,导致不知道完整的 url 请求或者参数是否请求正确,虽然我们可以进行抓包,但是抓包也是很麻烦的一件事情。然而有了 interceptor 拦截器,一切就变得简单起来了。
拦截器分为 应用拦截器 和 网络拦截器 两种,这里只讲 应用拦截器。
拦截器的原理很简单,就是在进行请求之前,过一道门进行判断是否可以进出,类似于进地铁时的安检一样。
日志拦截器的功能很简单,就是将网络请求的 url 及其参数/请求头之类输出即可,代码如下:
class LoggingInterceptor : Interceptor {
internal var tag = "LoggingInterceptor"
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val t1 = System.nanoTime()
println(
String.format(
"Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()
)
)
// Log.d(tag, );
val response = chain.proceed(request)
val t2 = System.nanoTime()
println(
String.format(
"Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6, response.headers()
)
)
return response
}
}
其中网络请求的url 及参数详情都在 Interceptor.Chain 参数之中
然后在对 okhttp 进行初始化的时候将日志拦截器设置进去接口,即调用:
addInterceptor(LoggingInterceptor())
方法。
此时在请求网络的时候,我们便可以在控制台中清晰的看到请求的网络信息了
更换 baseUrl
在实际开发中,经常会遇到,所有请求的接口不是部署在一个服务器中,而是部署在多个服务器中,此时请求的 baseUrl 就会不同。如本项目中,请求普通的新闻数据时,其 baseUrl 为 http://c.m.163.com/ ,然而请求图片新闻的 baseUrl 为: http://pic.news.163.com/
一个简单的方法是创建多个 retrofit 实例,但是如果存在多个 baseUrl 的情况下,会造成极大的资源浪费。那么可否在运行的时候动态的更改 retrofit 的baseUrl 呢?答案是可以的,我们可以借助 Interceptor 拦截器,在请求的时候判断是否需要更改 baseUrl .
步骤如下:
- 在请求接口中添加特殊请求头 Headers 给拦截器判断此次请求是否要更换 baseUrl
- 拦截器获取到更换 baseUrl 的请求头后,删除此请求头,更换 baseUrl
- 进行请求
那么就开始实施吧,首先第一步,我们设置网络请求头的 key 为 url_change ,value 则为目标 baseUrl ,在 NetEasyService 中添加请求图片的接口,如下:
interface NetEasyService {
......
// http://pic.news.163.com/photocenter/api/list/0003/00AJ0003,0AJQ0003,3LF60003,00B70003,00B50003/0/20.json
@Headers("url_change: " + Api.PictureBaseUrl) // 设置 headers, 然后在 Interceptor 中修改 baseUrl
@GET("photocenter/api/list/{vid}/{column}/{startIndex}/20.json")
fun getPicBean(@Path("vid") vid: String,
@Path("column") column: String,
@Path("startIndex") startIndex:Int): Call<String>
......
}
然后设置更换 baseUrl 的拦截器,如下:
class BaseUrlInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//获取request
val request = chain.request()
//从request中获取原有的HttpUrl实例oldHttpUrl
val oldHttpUrl = request.url()
//获取request的创建者builder
val builder = request.newBuilder()
//从request中获取headers,通过给定的键 url_name
val headerValues = request.headers("url_change")
if (headerValues != null && headerValues.size > 0) {
//如果有这个header,先将配置的header删除,因此header仅用作app和okhttp之间使用
val newUrl = headerValues[0]
builder.removeHeader("url_change")
if (TextUtils.isEmpty(newUrl)){
throw RuntimeException("new Url is Empty, if header contain url_change ,the value must not be empty!")
}
//匹配获得新的BaseUrl
val newBaseUrl = HttpUrl.parse(newUrl)
if (newBaseUrl != null) {
//重建新的HttpUrl,修改需要修改的url部分
val newFullUrl = oldHttpUrl
.newBuilder()
.scheme(newBaseUrl.scheme())//更换网络协议
.host(newBaseUrl.host())//更换主机名
.port(newBaseUrl.port())//更换端口
.build()
//重建这个request,通过builder.url(newFullUrl).build();
// 然后返回一个response至此结束修改
Log.e("Url", "intercept: $newFullUrl")
return chain.proceed(builder.url(newFullUrl).build())
}
}
return chain.proceed(request)
}
}
最后让 okhttp 添加拦截器即可,okhttp是支持多个拦截器的。
此时再次请求,我们就能看到在运行过程中,请求网络信息时, baseUrl 变更了。
retrofit 混合转换器
实际开发中也会遇到一种请求,不同的接口会返回不同数据格式,如一个接口返回 json,一个返回 string。然而目前 retrofit 只支持单一的转换器,也就是说设置了 GsonConverterFactory 转换器之后,无法再设置 simplexml 转换器。当解析 xml 时就直接崩溃。那么如何解决此问题呢?当然是自定义一个混合转换器了。此次参照了这边博文:
https://blog.csdn.net/cheng545/article/details/90550165
这里就不在多做解释了。
好了,网络封装部分基本完成,剩下的就是重复劳动,将需要的网络接口写入即可了。此次课程的效果如下:

如果大家对于项目的后续感兴趣可以关注我的个人微信公众号,每次更新都会在上面进行推送,还可以加入QQ群共同进步呢
微信公众号: Android实战之旅
微信号: unidirection
扫描关注:

也可以申请加群,大家一起沟通交流
QQ 群:799054441

也欢迎大家打赏,哈哈