Android面试题网络

第四课 Retrofit 网络封装

2019-07-31  本文已影响106人  宝塔山上的猫

项目源码地址
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 .

步骤如下:

  1. 在请求接口中添加特殊请求头 Headers 给拦截器判断此次请求是否要更换 baseUrl
  2. 拦截器获取到更换 baseUrl 的请求头后,删除此请求头,更换 baseUrl
  3. 进行请求

那么就开始实施吧,首先第一步,我们设置网络请求头的 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 

这里就不在多做解释了。

好了,网络封装部分基本完成,剩下的就是重复劳动,将需要的网络接口写入即可了。此次课程的效果如下:

network.gif

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

微信公众号: Android实战之旅

微信号: unidirection

扫描关注:

Android实战之旅

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

QQ 群:799054441

Android实战之旅群二维码.png

也欢迎大家打赏,哈哈

上一篇 下一篇

猜你喜欢

热点阅读