与okhttp不同的是,okhttp侧重的是底层通信的实现,而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库下载。
由于retrofit会借助gson将json数据转换为对象,因此我们需要新建一个app类:
class App(val id: String, val name: String, val version: String)
其次我们根据服务器接口的功能进行分类,以此创建不同的接口并在其中定义不同的接口方法。新建AppService接口:
interface AppService {
//此处注解表示调用getAppData方法是会发起get请求
//请求的地址是我们在get注解中传入的具体参数
@GET("get_data.json")
//此处的Call来自retrofit,服务器响应的是一个包含App数据的json数组
fun getAppData(): Call>
}
修改activity_main.xml:
修改MainActivity:
class MainActivity : AppCompatActivity(), View.OnClickListener {
private var resList = listOf(getAppDataBtn)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
for (res in resList) {
res.setOnClickListener(this)
}
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.getAppDataBtn -> {
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
//指定retrofit在解析数据时使用的转换库
.addConverterFactory(GsonConverterFactory.create())
.build()
//获得retrofit对象之后,我们可以调用他的create方法,并传入具体的service接口所对应的class
//传入后会创建一个该接口的动态代理对象,拥有之后可以随意调用接口中所定义的所有方法
val appService = retrofit.create(AppService::class.java)
//调用getAppData方法后,返回一个Call>对象
//此时调用enqueue方法,retrofit就会根据GET注解中配置的服务器地址去进行网络请求
//而请求后服务器返回的内容会回调到enqueue方法中传入的callback实现
//需要注意的是,当我们发起网络请求时,retrofit会自动开启内部的子线程
//数据回调到callback后,retrofit又会自动切换回主线程
appService.getAppData().enqueue(object : Callback> {
//此处是enqueue方法中传入的callback实现
override fun onResponse(call: Call>, response: Response>) {
//调用body方法获得返回的内容,即List类型的数据
val list = response.body()
if (list != null) {
for (app in list) {
Log.e("MainActivity", "id is ${app.id}, " +
"name is ${app.name}, " +
"version is ${app.version} ")
}
}
}
override fun onFailure(call: Call>, t: Throwable) {
t.printStackTrace()
}
})
}
}
}
}
为了让程序使用http,需要进行如下配置:右击res->New->Directory新建一个xml目录,接着右击xml->New->File,创建一个network_config.xml文件,写入以下内容:
此配置文件的意思是,允许我们以明文的方式在网络上传输数据,而http使用的就是明文传输方式。我们接着修改AndroidManifest.xm:
...
这里设置了允许使用明文的方式来进行网络请求,同时声明了网络权限。此时可运行。
真实情况下,服务器所提供的接口地址不可能像http://10.0.2.2/get_data.json这样如此简单。
为方便举例,此处先定义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
}
但服务器不可能总给我们提供静态的接口,在很多情况下,接口的部分内容会是动态变化的:
GET http://example.com//get_data.json
在此处,
interface ExampleService {
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call
}
在注解指定的接口中,使用了一个{page}的占位符,然后在getData方法中添加了一个page参数,并使用@Path("page")注解来声明这个参数。
这样,当我们调用getData方法发起请求时,retrofit会自动将page参数的值替换到占位符的位置,从而组成一个合法的请求地址。
此外,很多服务器接口会要求传入一系列参数,如:
GET http://example.com/get_data.json?u=&t=
这是一种标准的带参数GET请求的格式,接口地址最后的问号用于连接参数部分,而每个参数之间都是使用一个等号连接的键值对(即“key = value”的形式),多个参数之间使用“&”符号进行分割。
在示例的地址中,服务器要求传入user和token这两个参数的值,对于这种地址,我们可以使用刚才的@Path注解的方式,但会有些许麻烦,retrofit针对这种带参数的get请求,专门提供了一种语法支持:
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call
}
我们在getData方法中添加了user和token两个参数,并使用Query注解对他们进行声明。发起网络请求时,retrofit会自动按照带参数GET请求的格式,将这两个参数构建到请求地址中。
不过HTTP并不是只有GET请求这一种类型,而是有很多种,其中比较常用的有GET、POST、PUT、PATCH、DELETE这几种。它们之间的分工也很明确,简单概括的话,GET请求用于从服务器获取数据,POST请求用于向服务器提交数据,PUT和PATCH请求用于修改服务器上的数据,DELETE请求用于删除服务器上的数据。
比如服务器提供了以下接口:
DELETE http://example.con/data/
这样的接口通常意味着要根据id来删除一条指定的数据,我们想要用retrofit发送这样的请求可以这样写:
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id") id: String): Call
}
在返回值声明是,我们将Call的泛型指定为ResponseBody,这是因为与GET请求不同,其余请求是操作服务器中的数据,而不是获取,所以一般不关心服务器返回的数据,而ResponseBody表示retrofit能够接受任意类型的响应数据,且不对其进行解析。
如果我们需要提交数据到服务器,接口地址如下:
POST http://example.com/data/create
{"id": 1, "content": "The description for this data."}
我们使用POST请求来提交数据,此时需要将数据放到HTTP请求的body部分,我们可以使用@Body注解来实现:
interface ExampleService {
@POST("data/create")
fun createData(@Body data: Data): Call
}
此时我们在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
}
这样的写法只能对应静态的header,如果要动态指定header,需要使用@Header注解:
interface ExampleService {
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String): Call
}
现在当发起网络请求时,retrofit会自动将参数中传入的值,设置到User-Agent和CacheControl这两个header当中,从而实现了动态指定header值的功能。
想要得到AppService的动态代理对象,首先需要用Retrofit.Builder构建出一个retrofit对象,然后再调用这个对象的create方法创建动态代理对象。如果只是写一次还好,每次都要写未免太麻烦。
事实上构建出的retrofit对象是全局通用的,因此我们每次只需要在调用create方法是针对不同的service方法传入相应的Class类型即可。
新建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()
//当外部调用这个create方法时,实际上调用的就是retrofit对象的create方法
fun create(serviceClass: Class): T = retrofit.create(serviceClass)
}
此处我们将ServiceCreator定义为一个单例类,并在内部制定了一个BASE_URL常量用于指定retrofit的根路径。定义retrofit对象的方法是相同的,使用Retrofit.Builder方法构建。
需要注意的是,我们都使用private来声明这些对象,对于外部他们都是不可见的。最后,我们提供了一个外部可见的create方法,并接受一个Class类型的参数。
使用这种方式封装的retrofit用法会变得很简单,比如此时我们想获取一个AppService接口的动态代理对象:
val appService = ServiceCreator.create(AppService::class.java)
之后我们就可以随意使用AppService内的方法了。