Retrofit基本用法

  最好用的网络库:Retrofit。它和OkHttp的定位完全不同,OkHttp侧重的是底层通信的实现,而Retrofit侧重的是上层接口的封装。

  Retrofit就是Square公司在OkHttp的基础上进一步开发出来的应用层网络通信库,使得我们可以用更加面向对象的思维进行网络操作。

Retrofit的基本设计思想

  • 同一款应用程序中所发起的网络请求绝大多数指向的是同一个服务器域名。
  • 服务器提供的接口通常是可以根据功能来归类的。将服务器接口合理归类能够让代码结构变得更加合理,从而提高可阅读性和可维护性。
  • 开发者更加习惯于“调用一个接口,获取它的返回值”这样的编码方式,其实大多数人并不关心网络的具体通信细节,但是传统网络库的用法却需要编写太多网络相关的代码。

  而Retrofit的用法就是基于以上几点来设计的。

  首先我们可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可,这样就不用每次都指出完成的URL地址了。

  另外,Retrofit允许我们对服务器接口进行归类,就功能同属一类的服务器接口定义到同一个接口文件中。

  最后,我们也完全不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。

当我们在程序中调用该方法时,Retrofit会自动向对应的服务器接口发起请求,并将相应的数据解析成返回值声明的类型。这就使得我们可以用更加面向对象的思维来进行网络操作。

 

Retrofit的基本用法

添加依赖

implementation 'com.squareup.retrofit2:retrofit:2.6.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.1'

 

  第一条依赖是下载Retrofit、OkHttp和Okio这几个库,我们就不需要手动引入OkHttp库了;

  第二条依赖是一个Retrofit的转换库,它是借助GSON来解析JSON数据的,所以也会将GSON库一起下载。

 

基本用法

  新建一个App类:

class App(val id: String, val name: String, val version: String)

 

  然后,根据服务器接口的功能归类,创建不同种类的接口文件,并定义具体服务器接口的方法。不过目前本地服务器上只有一个获取JSON数据的接口,因此只需要定义一个接口文件,并包含一个方法即可,新建AppService接口:

interface AppService {
    
    @GET("get_data.json")
    fun getAppData(): Call<List<App>>
}

 

  通常Retrofit的接口文件建议以具体的功能种类名开头,并以Service结尾,这是一种比较好的命名习惯。

  这里需要注意两点:

    第一点:使用了一个@GET注解,表示当调用getAppData()方法时Retrofit会发起一条GET请求,请求的地址就是我们在@GET注解中传入的具体参数。这里只需要传入相对路径即可,根路径在其他地方设置。

    第二点:getAppData()方法的返回值必须声明成Retrofit中内置的Call函数,并通过泛型来指定服务器响应的数据应该转换成什么对象。由于服务器响应的是一个包含App数据的JSON数组,所以声明成List<App>。

  

  定义好了AppService接口之后,接下来就是使用它,修改MainActivity中的代码:

getAppDataBtn.setOnClickListener {
            val retrofit = Retrofit.Builder()
                .baseUrl("https://10.0.0.2/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            val appService = retrofit.create(AppService::class.java)
            appService.getAppData().enqueue(object : Callback<List<App>> {
                override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
                    val list = response.body()
                    if (list != null) {
                        for (app in list) {
                            Log.d("MainActivity", "id is ${app.id}")
                            Log.d("MainActivity", "name is ${app.name}")
                            Log.d("MainActivity", "version is ${app.version}")
                        }
                    }
                }

                override fun onFailure(call: Call<List<App>>, t: Throwable) {
                    t.printStackTrace()
                }
            })
        }

 

  首先,使用Retrofit.Builder来构建一个Retrofit对象,其中baseUrl()方法指定所有Retrofit请求的根路径,addConverterFactory()方法指定Retrofit在解析数据时所使用的转换库,这里指定成GsonConverterFactory。这两个方法是必须调用的。

  有了Retrofit对象,调用它的create()方法,并传入具体Service接口所对应的Class类型,创建一个该接口的动态代理对象。有了这个动态代理对象,就可以随意调用接口中定义的所有方法了,而Retrofit会自动执行具体的处理。

  当调用了AppService的getAppData()方法时,会返回一个Call<List<App>>对象,此时再调用一下它的enqueue()方法,Retrofit就会根据注解中配置的服务器接口地址去进行网络请求了,服务器响应的数据会回调到enqueue()方法中传入的Callback实现里面。

注意:当发起请求时,Retrofit会自动在内部开启子线程,当数据回调到Callback中之后,Retrofit又会自动切换回主线程。整个过程我们是不用考虑进程切换问题的。

  在Callback的onResponse()方法中,调用response.body()方法将会得到Retrofit解析后的对象,也就是<List<App>>类型的数据,最后遍历即可。

 

 

* 处理复杂的接口地址类型

  先定义一个Data类,有id、content两个字段:

class Data(val id: String, val content: String)

  比如服务器的接口地址如下:

GET http://example.com/get_data.json

  这是最简单的一种情况,接口地址是静态的,不会改变。那么对应到Retrofit中,写法如下:

interface ExampleService {
    @GET("get_data.json")
    fun getData(): Call<Data>
}

 

  但是很显然服务器不可能总是提供静态类型的接口,很多时候,接口地址中的部分内容是动态变化的,比如:

GET http://example.com/<page>/get_data.json

  这个接口中,<page>代表页数,传入不同的页数,服务器返回的数据也不同。对应到Retrofit就该这么写:

interface ExampleService {
    @GET("{page}/get_data.json")
    fun getData(@Path("page") page: Int) : Call<Data>
}

 

  在@GET注解指定的接口地址中,这里使用了一个{page}的占位符,然后又在getData()方法中添加一个page参数,并使用@Path("page")注解来声明这个参数。这样当调用getData()方法发起请求时,Retrofit就会自动将page参数的值替换到占位符的位置,从而组成一个合法的请求地址。

  另外,很多服务器接口还会要求我们传入一系列的参数,格式如下:

GET http://example.com//get_data.json?u=<user>&t=<token>

  这是一种标准的带参数GET请求格式。接口地址的最后用问号来连接参数部分,每个参数是用一个等号连接的键值对,多个参数之间用 ’&‘ 符号分割。这种情况我们可以使用刚才的@Path注解方式,不过有些麻烦。还好Retrofit对这种带参数的GET请求,专门提供了一种语法:

interface ExampleService {
    @GET("get_data.json")
    fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}

 

  在getData()方法中添加了user和token参数,并使用@Query注解对它们进行声明。这样当发起网络请求时,Retrofit就会自动按照带参数GET请求的格式将这两个参数构建到请求地址中。

  掌握了以上内容,基本上可以应对各种千变万化的服务器接口地址了。

  不过HTTP并不是只有GET请求这一种类型,还有POST、PUT、PATCH、DELETE等。它们之间的分工也很明确:

  GET请求用于从服务器获取数据;

  POST请求用于向服务器提交数据;

  PUT和PATCH请求用于修改服务器上的数据;

  DELETE请求用于删除服务器上的数据。

  而Retrofit对所有常用的HTTP请求类型都进行了支持,使用@GET、@POST、@PUT、@PATCH、@DELETE注解,就可以让Retrofit发出相应类型的请求了。

 

删除数据

  比如服务器提供了如下接口地址:

DELETE http://example.com/data/<id>

 

  意味着根据id删除一条指定的数据,写法如下:

interface ExampleService {
    @DELETE("data/{id}")
    fun deleteData(@Path("id") id: String): Call<ResponseBody>
}

 

  这里不用的是,将Call泛型指定成了ResponseBody,为什么呢?

  由于POST、PUT 、PATCH、DELETE这几种请求类型与GET请求不同,它们更多是用于操作服务器上的数据,而不是获取服务器上的数据,所以通常它们对于服务器响应的数据并不关心。这个时候就可以使用ResponseBody,表示Retrofit能够接收任意类型的响应数据,并且不会对响应数据进行解析。

 

提交数据

  接口地址如下:

POST http://example.com/data/create
{"id": 1, "content": "This is a data."}

  使用POST请求提交数据,需要将数据放到HTTP请求的body部分,在Retrofit中借助@Body注解来完成:

interface ExampleService {
    @POST("data/create")
    fun createData(@Body data: Data): Call<ResponseBody>
}

 

  在createData()方法中声明了一个Data类型的参数,并给它加上了@Body注解。这样当Retrofit发出POST请求时,就会自动将Data对象中的数据转换成JSON格式的文本,并放到HTTP请求的body部分,服务器在收到请求之后只需要从body中将这部分数据解析出来即可。这种写法同样也可以用来给PUT、PATCH、DELETE类型的请求提交数据。

 

指定Header

  有些服务器接口会要求我们在HTTP请求的header中指定参数,比如:

GET http://example.com/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0

 

  这些header参数其实就是一个个键值对,只需要在Retrofit中用@Headers注解来声明即可:

interface ExampleService {
    @Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
    @GET("get_data.json")
    fun getData(): Call<Data>
}

  但是这种写法只适用于静态header声明,如果想要动态指定header的值,则需要用到@Header注解,如下:

interface ExampleService {
    @GET("get_data.json")
    fun getData(@Header("User-Agent") userAgent: String,
            @Header("Cache-Control") cacheControl: String): Call<Data>
}

  现在发起网络请求的时候,Retrofit就会自动将参数中传入的值设置到User-Agent和Cache-Control这两个Header中,实现了动态指定功能。

  目前为止,已经将使用Retrofit处理复杂接口地址类型的内容基本学完了。