地址:GitHub - square/retrofit: A type-safe HTTP client for Android and the JVM
一、简介
它和OkHttp的定位完全不同。OkHttp 侧重的是底层通信的实现,而Retrofit侧重的是上层接口的封装。事实上,Retrofit就是Square 公司在OkHttp的基础上进一步开发出来的应用层网络通信库,使得我们可以用更加面向对象的 思维进行网络操作
添加必要的依赖库
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是最常用的。
实例
创建数据接收模型
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。当然,Retrofit还提供了强大的Call Adapters功 能来允许我们自定义方法返回值的类型,比如Retrofit结合RxJava使用就可以将返回值声明成 Observable、Flowable等类型,不过这些内容就不在本节的讨论范围内了。 定义好了AppService接口之后,接下来的问题就是该如何使用它。为了方便测试,
我们还得 在界面上添加一个按钮才行。修改activity_main.xml中的代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/getAppDataBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Get App Data" />
</LinearLayout>
很简单,这里在布局文件中增加了一个Button控件,我们在它的点击事件中处理具体的网络请 求逻辑即可
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.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()
}
})
}
}
}
在“Get App Data”按钮的点击事件当中,首先使用了Retrofit.Builder来构建 一个Retrofit对象,其中baseUrl()方法用于指定所有Retrofit请求的根路径, addConverterFactory()方法用于指定Retrofit在解析数据时所使用的转换库,这里指定成 GsonConverterFactory。注意这两个方法都是必须调用的。 有了Retrofit对象之后,我们就可以调用它的create()方法,并传入具体Service接口所对应 的Class类型,创建一个该接口的动态代理对象。如果你并不熟悉什么是动态代理也没有关 系,你只需要知道有了动态代理对象之后,我们就可以随意调用接口中定义的所有方法,而 Retrofit会自动执行具体的处理就可以了
对应到上述的代码当中,当调用了AppService的getAppData()方法时,会返回一个 Call>对象,这时我们再调用一下它的enqueue()方法,Retrofit就会根据注解 中配置的服务器接口地址去进行网络请求了,服务器响应的数据会回调到enqueue()方法中传 入的Callback实现里面。需要注意的是,当发起请求的时候,Retrofit会自动在内部开启子线 程,当数据回调到Callback中之后,Retrofit又会自动切换回主线程,整个操作过程中我们都 不用考虑线程切换问题。在Callback的onResponse()方法中,调用response.body()方 法将会得到Retrofit解析后的对象,也就是List类型的数据,最后遍历List,将其中的数 据打印出来即可。 接下来就可以进行一下测试了,不过由于这里使用的服务器接口仍然是HTTP,因此我们还要按 照步骤来进行网络安全配置才行。先从NetworkTest项目中复制 network_config.xml文件到RetrofitTest项目当中,然后修改AndroidManifest.xml中的代 码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.retrofittest">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_config">
...
</application>
</manifest>
设置了允许使用明文的方式来进行网络请求,同时声明了网络权限
二、处理复杂的接口地址类型
为了方便举例,这里先定义一个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
在这个接口当中,部分代表页数,我们传入不同的页数,服务器返回的数据也会不同。 这种接口地址对应到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请求的格式。接口地址的最后使用问号来连接参数部分,每个参数都 是一个使用等号连接的键值对,多个参数之间使用“&”符号进行分隔。那么很显然,在上述地址 中,服务器要求我们传入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删除一条指定的数据,而我们在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和CacheControl这两个header当中,从而实现了动态指定header值的功
四、Retrofit构建器
新建一个ServiceCreator单例类
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
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 = ServiceCreator.create(AppService::class.java)
之后就可以随意调用AppService接口中定义的任何方法了。 不过上述代码其实仍然还有优化空间,。修改ServiceCreator中的代码
object 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>()