注:第一行代码(3版)
Retrofit 同样是一款有 Square 公司开发的网络库,但是它和 OkHttp 的定位完全不同。OkHttp 侧重的是底层通信的实现,而 Retrofit 侧重的是上层接口的封装。事实上,Retrofit 就是 Square 公司在 OkHttp 的基础上进一步开发出来的应用层网络通信库,使得我们可以用更加面向对象的思维进行网络操作。项目主页地址。
Retrofit 的基本用法
Retrofit 的设计基于以下几个事实。
同一款应用程序中所发起的网络请求绝大多数指向的是同一个服务器域名。这个很好理解,因为任何公司的产品,客户端和服务器都是配套的。
另外,服务器提供的接口通常是可以根据功能来归类的。比如新增用户、修改用户数据、查询用户数据这几个接口就可以归为一类,上架新书、销售图书、查询可供销售图书这几个接口也可以归为一类。将服务器接口合理归类能够让代码结构变得更加合理,从而提供可阅读性和可维护性。
最后,开发者肯定更加习惯于 “调用一个接口,获取它的返回值” 这样的编码方式,但当调用的是服务器接口时,却很难想象该如何使用这样的编码方式。其实大多数人并不关心网络的具体通信细节,但是传统网络库的用法却需要编写太多网络相关的代码。
而 Retrofit 的用法就是基于以上几点来设计的,首先我们可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可,这样就不用每次都指定完整的 URL 地址了。
另外 Retrofit 允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件当中,从而让代码结构变得更加合理。
最后,我们也完全不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们在程序中调用该方法时,Retrofit 会自动向对应的服务器接口发起请求,并将响应的数据解析成返回值声明的类型。这就使得我们可以用更加面向对象的思维来进行网络操作。
Retrofit 的基本设计思想差不多就是这些,下面就让我们通过一个具体的例子来快速体验一下 Retrofit 的用法。
要想使用 Retrofit,我们需要先在项目中添加必要的依赖库。编辑 app/build.gradle 文件,在 dependencies 闭包中添加如下内容:
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
由于 Retrofit 是基于 OkHttp 开发的,因此添加上述第一条依赖会自动将 Retrofit、OkHttp 和 Okio 这几个库一起下载,我们无需再手动引入 OkHttp 库。另外,Retrofit 还会将服务器返回的 JSON 数据自动解析成对象,因此上述第二条依赖就是一个 Retrofit 的转换库,它是借助 GSON 来解析 JSON 数据的,所以会自动将 GSON 库一起下载下来,这样我们也不用手动引入 GSON 库了。除了 GSON 之外,Retrofit 还支持各种其他主流的 JSON 解析库,包括 Jackson、Moshi 等,不过毫无疑问 GSON 是最常用的。
搭建一个最简单的 Web 服务器用来测试。由于 Retrofit 会借助 GSON 将 JSON数据转换成对象,因此这里同样需要新增一个 App 类,并加入 id、name 和 version 这 3 个字段,如下所示:
class App(val id: String, val name: String, val version: String)
接下来,我们可以根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中 定义对应具体服务器接口的方法。不过由于我们的 Apache 服务器上其实只有一个获取 JSON 数据的接口,因此这里只需要定义一个接口文件,并包含一个方法即可。新建 AppService 接口,代码如下所示:
interface AppService {
@GET("get_data.json")
fun getAppData(): Call<List<App>>
}
通常 Retrofit 的接口文件建议以具体的功能种类名开头,并以 Service 结束,这是一种比较好的命名习惯。
上述代码中有两点需要我们注意。第一就是在 getAppData() 方法上面添加的注解,这里使用了一个 @GET 注解,表示当调用 getAppData() 方法时 Retrofit 会发起一条 GET 请求,请求的地址就是我们在 @GET 注解中传入的具体参数。注意,这里只需要传入请求地址的相对路径即可,根路径我们会在稍后设置。
第二就是 getAppData() 方法的返回值必须声明成 Retrofit 中内置的 Call 类型,并通过泛型来指定服务器响应的数据应该转换成什么对象。由于服务器响应的是一个包含 App 数据的 JSON 数组,因此这里我们将泛型声明成 List<App>。当然,Retrofit 还提供了强大的 Call Adapters 功能来允许我们自定义方法返回值的类型,比如 Retrofit 结合 RxJava 使用就可以将返回值声明成 Observable、Flowable 等类型,不过这些内容就不在这里讨论了。
定义好了 AppService 接口之后,接下来的问题就是该如何使用它。为了方便测试,我们在界面上添加一个按钮。
<Button
android:id="@+id/getAppDataBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Get App Data" />
现在修改 Activity 中的代码,如下所示:
class RetrofitActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_retrofit)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.1.176/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback<List<App>> {
override fun onFailure(call: Call<List<App>>, t: Throwable) {
t.printStackTrace()
}
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
val list = response.body()
if (list != null) {
for (app in list) {
Log.d("RetrofitActivity", "id is ${app.id}")
Log.d("RetrofitActivity", "name is ${app.name}")
Log.d("RetrofitActivity", "version is ${app.version}")
}
}
}
})
}
}
}
可以看到,在 “Get App Data” 按钮的点击事件当中,首先使用了 Retrofit.Builder 来构建一个 Retrofit 对象,其中 baseUrl() 方法用于指定所有 Retrofit 请求的根路径,addConverterFactory() 方法用于指定 Retrofit 在解析数据时所使用的的转换库,这里指定成 GsonConverterFactory。注意这两个方法都是必须调用的。
有了 Retrofit 对象之后,我们就可以调用它的 create() 方法,并传入具体 Service 接口所对应的 Class 类型,创建一个该接口的动态代理对象。如果你并不熟悉什么是动态代理也没有关系,你只需要知道有了动态代理对象之后,我们就可以随意调用接口中定义的所有方法,而 Retrofit 会自动执行具体的处理就可以了。
对应到上述的代码当中,当调用了 AppService 的 getData() 方法时,会返回一个 Call<List<App>> 对象,这时我们在调用一下它的 enqueue() 方法,Retrofit 就会根据注解中配置的服务器接口地址去进行网络请求了,服务器响应的数据会回调到 enqueue() 方法中传入的 Callback 实现里面。需要注意的是,当发起请求的时候,Retrofit 会自动在内部开启子线程,当数据回调到 Callback 中之后,Retrofit 又会自动切换回主线程,整个操作过程中我们都不用考虑线程切换问题。在 Callback 的 onResponse() 方法中,调用 response.body() 方法将会得到 Retrofit 解析后的对象,也就是 List<App> 类型的数据,最后遍历 List,将其中的数据打印出来即可。
接下来就可以进行测试了,不过由于这里使用的服务器接口仍然是 HTTP,因此我们还需要进行网络安全配置才行。配置完成后,就可以运行程序,点击按钮,观察 Logcat 中的日志,如下图所示:
可以看到,服务器响应的数据已经被成功解析出来了。以上就是使用 Retrofit 进行网络操作的基本写法。
处理复杂的接口地址类型
在真实的开发环境中,服务器所提供的接口地址可能会是千变万化的。
为了方便举例,这里先定义一个 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//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=&t=
这是一种标准的带参数 GET 请求的格式。接口地址的最后使用问号来连接参数部分,每个参数都是一个使用等号连接的键值对,多个参数之间使用 “&” 符号进行分隔。那么很显然,在上述地址中,服务器要求我们传入 user 和 token 这两个参数的值。对于这种格式的服务器接口,我们可以使用刚才所学的 @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 请求这一类型,而是有很多种,其中比较常用的有 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 删除一条指定的数据,而我们在 Retrofit 当中想要发出这种请求就可以这样写:
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id") id: String): Call<ResponseBody>
}
这里使用了 @DELETE 注解来发出 DELETE 类型的请求,并使用了 @Path 注解来动态指定 id,这些都很好理解。但是在返回值声明的时候,我们将 Call 的泛型指定成了 ResponseBody,这是什么意思呢?
由于 POST、PUT、PATCH、DELETE 这几种请求类型与 GET 请求不同,它们更多是用于操作服务器上的数据,而不是获取服务器上的数据,所以通常它们对于服务器响应的数据并不关心。这个时候就可以使用 ResponseBody,表示 Retrofit 能够接收任意类型的响应数据,并且不会对响应数据进行解析。
那么如果我们需要向服务器提交数据该怎么写呢?比如如下的接口地址:
POST http://example.com/data/create
{“id”:1,“content”:“The description for this 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 类型的请求提交数据。
最后,有些服务器接口还可能会要求我们在 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 当中,从而实现了动态指定 header 值的功能。
Retrofit 构建器的最佳写法
学到这里,其实还有一个问题我们没有正视过,就是获取 Service 接口的动态代理对象实在是太麻烦了。先回顾一下之前的写法吧,大致代码如下所示:
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.1.176/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
我们想要得到 AppService 的动态代理对象,需要先使用 Retrofit.Builder 构建出一个 Retrofit 对象,然后再调用 Retrofit 对象的 create() 方法创建动态代理对象。如果只是写一次还好,每次调用任何服务器接口时都要这样写一遍的话,肯定没有人能受得了。
事实上,确实也没有每次都写一遍的必要,因为构建出的 Retrofit 对象是全局通用的,只需要在调用 create() 方法时针对不同的 Service 接口传入相应的 Class 类型即可。因此,我们可以将通用的这部分功能封装起来,从而简化获取 Service 接口动态代理的过程。
新建一个 ServiceCreator 单例类,代码如下所示:
object ServiceCreator {
private const val BASE_URL = "http://192.168.1.176/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
}
这里我们使用 object 关键字让 ServiceCreator 成为了一个单例类,并在它的内部定义了一个 BASE_URL 常量,用于指定 Retrofit 的根路径。然后同样是在内部使用 Retrofit.Builder 构建一个 Retrofit 对象,注意这些都是用 private 修饰符来声明的,相当于对于外部而言它们都是不可见的。
最后,我们提供了一个外部可见的 create() 方法,并接受一个 Class 类型的参数。当在外部调用这个方法时,实际上就是调用了 Retrofit 对象的 create() 方法,从而创建出相应 Service 接口的动态代理对象。
经过这样的封装之后,Retrofit 的用法将会变得异常简单,比如我们想要获取一个 AppService 接口的动态代理对象,只需要使用如下写法即可:
val appService = retrofit.create(AppService::class.java)
之后就可以随意调用 AppService 接口中定义的任何方法了。
不过上述代码其实仍然还有优化空间,根据 Kotlin 中的泛型实化功能。修改 ServiceCreator 中的代码,如下所示:
inline fun <reified T> create(): T = create(T::class.java)
可以看到,我们又定义了一个不带参数的 create() 方法,并使用 inline 关键字来修饰方法,使用 reified 关键字来修饰泛型,这是泛型实化的两大前提条件。接下来就可以使用 T::class.java 这种语法了,这里调用刚才定义的带有 Class 参数的 create() 方法即可。
那么现在我们就有了一种新的方式来获取 AppService 接口的动态代理对象,如下所示:
val appService = ServiceCreator.create<AppService>()
以上就是 Retrofit 的一些使用方法。后面还需要学习如何在实际的项目中使用 Retrofit。