前言
Retrofit 同样是由 Square 公司开发的网络库。但它和 OkHttp 的定位不同,OkHttp 侧重于实现底层的 HTTP 通信,而 Retrofit 则侧重于上层接口的封装。
实际上,Retrofit 就是在 OkHttp 的基础上开发的一个应用层网络库。它能让我们以一种更加面向对象和声明式的方式进行网络操作,大大提高了开发效率和代码可读性。
Retrofit 的项目地址:retrofit on github
Retrofit 的核心设计思想
我们先来看看 Retrofit 的设计思想,其设计主要基于以下三点:
-
同一个应用发起的网络请求,绝大多数都指向同一个服务器域名。
-
服务器提供的接口通常可按照功能模块进行划分。
-
开发者习惯于像调用一个方法那样,直接调用服务器接口、传递参数并获取返回值。并不想关心底层的通信细节。
Retrofit 的用法正好对应了以上三点:
-
我们配置一个
Base Url根路径。这样后续在指定接口地址时,只需使用相对路径。 -
我们可以将功能相近的接口定义到同一个接口文件中,通过注解来描述 HTTP 请求的行为。
-
我们只需像调用普通方法一样调用这些接口方法,Retrofit 会自动发送网络请求,并将服务器响应的数据解析成我们指定的类型。
基础用法:发起网络请求
我们通过一个具体的例子来演示一下 Retrofit 的基本用法。
使用回调方式
首先新建一个名为 RetrofitTest 的 Empty Views Activity 项目。在 app/build.gradle.kts 文件中,添加对应的库依赖。如下所示:
dependencies {
// Retrofit 核心库(它会自动引入其依赖的 OkHttp 和 Okio 库)
implementation("com.squareup.retrofit2:retrofit:2.6.1")
// Retrofit 的 GSON 转换器,用于 JSON 数据的序列化和反序列化
implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}
假设本地服务器接口 (http://10.0.2.2/get_data.json) 会返回如下的 JSON 数组:
[
{ "id": "5", "version": "5.5", "name": "Clash of Clans" },
{ "id": "6", "version": "7.0", "name": "Boom Beach" },
{ "id": "7", "version": "3.5", "name": "Clash Royale" }
]
由于 Retrofit 会借助 GSON 将上面的 JSON 数据转为对象,所以我们需要定义一个对应的数据类:
data class App(val id: String, val name: String, val version: String)
接着,定义一个 AppService 接口,在其中定义网络请求接口方法。代码如下:
interface AppService {
@GET("get_data.json")
fun getAppData(): Call<List<App>>
}
Retrofit 的接口文件名建议以功能开头,Service 结尾,比如 UserService。
代码中的 @GET 注解表示调用 getAppData() 方法时,会发送 GET 请求。注解的参数 get_data.json 为请求路径 ,这只是个相对于 Base Url 根路径的路径。
在使用回调方式时,方法的返回值必须声明为 Retrofit 内置的 Call<T> 类型,我们通过泛型 T 来指定服务器响应数据转换后的类型。
定义好接口后,我们就可以使用了。首先在布局中放置一个按钮,用于触发请求,activity_main.xml 文件中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/getAppDataBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Get App Data" />
</LinearLayout>
然后,在 MainActivity 中实现按钮的点击逻辑。代码如下:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 构建 Retrofit 对象
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("http://10.0.2.2/") // 设置请求路径的根路径
.addConverterFactory(GsonConverterFactory.create()) // 设置解析数据时,使用的转换库
.build()
}
// 创建 AppService 接口的动态代理对象,用于调用接口中的方法
private val appService: AppService by lazy {
retrofit.create(AppService::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.getAppDataBtn.setOnClickListener {
// 调用 enqueue() 方法,进行异步的网络请求
appService.getAppData().enqueue(object : Callback<List<App>> {
// 请求成功后回调
override fun onResponse(
call: Call<List<App>>,
response: Response<List<App>>,
) {
// HTTP 状态码是否在 200-299 之间
if (response.isSuccessful) {
val appList = response.body()
appList?.let { list ->
list.forEach { app ->
Log.d(
"MainActivity",
"id is ${app.id},name is ${app.name},version is ${app.version}"
)
}
}
} else {
// 处理服务器返回的错误
Log.e("MainActivity", "Response failed with code: ${response.code()}")
}
}
// 在网络异常或数据解析异常时回调
override fun onFailure(call: Call<List<App>>, t: Throwable) {
Log.e("MainActivity", "Request failed", t)
t.printStackTrace()
}
})
}
}
}
然后,别忘了声明网络权限,并且我们使用明文的 http 进行网络请求,需要添加网络安全配置。
AndroidManifest.xml 文件中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
<application
...
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
</manifest>
在 res/xml 目录中创建 network_security_config.xml 文件,其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
现在运行应用并点击按钮,Logcat 日志信息如下:
D/MainActivity com.example.retrofittest id is 5,name is Clash of Clans,version is 5.5
D/MainActivity com.example.retrofittest id is 6,name is Boom Beach,version is 7.0
D/MainActivity com.example.retrofittest id is 7,name is Clash Royale,version is 3.5
可以看到,服务器响应的数据成功被解析出来了。
使用协程方式
虽然使用回调的方式能够工作,但存在多个互相依赖的请求时,很容易形成“回调地狱”。现在,我们更多会使用协程来完成,Retrofit 对协程提供了原生支持。
首先,我们在 app/build.gradle.kts 文件中添加协程的依赖:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}
然后修改之前的 AppService 接口。在方法声明前加上 suspend 关键字,将返回值类型指定为我们期望的类型。
interface AppService {
@GET("get_data.json")
suspend fun getAppData(): List<App>
}
在 MainActivity 中使用协程来调用这个方法:
binding.getAppDataBtn.setOnClickListener {
// 启动一个协程
lifecycleScope.launch {
try {
val appList = appService.getAppData()
// 请求成功,获得解析后的数据
for (app in appList) {
Log.d("MainActivity", "[Coroutine] id is ${app.id}, name is ${app.name}")
}
} catch (e: Exception) {
// 捕获所有的异常(网络异常、解析失败、服务器错误)
Log.e("MainActivity", "[Coroutine] Request failed", e)
e.printStackTrace()
}
}
}
可以看到,使用协程后,代码非常简洁,逻辑非常清晰。
进阶用法:动态参数与请求类型
之前的接口地址是静态的,不会改变。而在真实项目中,接口地址中的部分内容可能是动态变化的,请求也需要携带各种参数。
动态替换 URL 路径:@Path
就比如根据文章 id 获取文章内容的接口地址可能为:https://api.example.com/articles/<id>,其中 <id> 是占位符,代表文章 id。这种接口地址对应到 Retrofit 中,只需这么写:
data class Article(val id: String, val title: String, val content: String)
interface ArticleService {
@GET("articles/{id}")
suspend fun getArticle(@Path("id") articleId: String): Article
}
在接口地址中,我们使用了 {id} 的占位符。然后给 getArticle() 方法定义了一个 articleId 参数,并使用了 @Path("id") 注解将其与路径中的 {id} 占位符进行绑定。这样当调用 getArticle() 方法时,Retrofit 会自动将 articleId 参数的值传给 {id} 占位符,从而生成一个请求地址。
添加查询参数:@Query
另外,我们可能会在请求路径后通过查询参数来传递信息,比如 GET https://api.example.com/search?q=<keyword>&sort_by=<date>。接口地址中 ? 的后面就是查询参数,每个参数都是键值对的形式,参数之间使用 & 分隔。
虽然我们可以使用 @Path 拼接字符串来完成,但这稍微有点繁琐还容易出错。Retrofit 针对于这种情况,提供了@Query 注解。
interface SearchService {
@GET("search")
suspend fun searchArticles(
@Query("q") keyword: String,
@Query("sort_by") sortBy: String = "date" // 还可以提供默认值
): List<Article>
}
我们使用 @Query 注解来声明 keyword 和 sortBy 参数。这样当调用该方法时,会自动将这两个参数填入到请求地址中。
HTTP 请求类型
我们看完了最常见的 GET 请求,但 HTTP 的请求方法有很多,常用的有 GET、POST、PUT、PATCH、DELETE。
-
GET用于从服务器中获取数据。 -
POST用于向服务器提交数据。 -
PUT和PATCH用于修改服务器上的数据,不过前者用于完整替换,后者用于部分更新。 -
DELETE用于删除服务器中的数据。
Retrofit 对以上的 HTTP 请求类型都做了支持,分别对应 @GET、@POST、@PUT、@PATCH、@DELETE 注解。
删除数据:@DELETE
删除操作需要在路径中指定要删除的 id:
interface ArticleService {
@DELETE("articles/{id}")
suspend fun deleteArticle(@Path("id") articleId: String): Response<ResponseBody>
}
返回值类型我们指定为了 Response<ResponseBody>,这是因为删除操作通常不关心服务器返回的具体数据内容,只关心是否成功。而 ResponseBody 是一个通用的类型,能够接收任意类型的响应体。使用它,能够避免 Retrofit 不必要的 JSON 数据解析。
另外,将 ResponseBody 包装在 Response<> 中,能让我们获取完整的响应信息(如 HTTP 状态码)。
提交数据:@POST 与 @Body
如果要向服务器提交数据,我们需要将数据放到 HTTP 请求的请求体(Request Body)中,通过 @Body 注解来指定哪个对象作为请求体。
data class NewArticle(val title: String, val content: String, val authorId: String)
interface ArticleService {
// 创建成功后会返回新创建的文章对象
@POST("articles")
suspend fun createArticle(@Body article: NewArticle): Response<Article>
}
当调用 createArticle() 方法时,会将参数传入的 NewArticle 对象转为 JSON 格式的字符串,并放入到 HTTP 请求的请求体中发送给服务器。
添加请求头:@Headers 与 @Header
最后,服务器接口可能会要求在 HTTP 请求的请求头(Headers)中包含特定信息。我们可以通过 @Headers 注解在方法上声明:
interface DataService {
@Headers(
"User-Agent: MyCoolAndroidApp/1.0",
"Cache-Control: max-age=60"
)
@GET("data.json")
suspend fun getData(): Data
}
这种写法只适用于请求头的值是固定的情况。如果要动态指定请求头的值,需要用到 @Header 注解,将其与方法参数进行绑定。
interface DataService {
@GET("user/profile")
suspend fun getUserProfile(
@Header("Authorization") token: String,
@Header("Accept-Language") lang: String
): UserProfile
}
当调用 getUserProfile() 方法时,会自动将参数的值添加到请求头中。
创建统一的 Service 入口
在前面的例子中,我们直接在 Activity 中创建了 Retrofit 实例。在真实项目中,我们并不会这样做,因为:
-
每个
Retrofit实例,尤其是它内部的OkHttpClient,都管理着自己的连接池和线程池。创建多个实例将无法复用这些资源,会导致性能下降。 -
如果需要修改服务器根路径,或是为所有请求统一添加请求头,你必须要在项目中找到并修改每一个
Retrofit实例。
为此,我们需要一个统一、全局的入口来创建和管理我们的网络请求服务。这里我们通过一个 ServiceCreator 单例对象来实现:
首先,需要添加 OkHttp 的日志拦截器依赖:
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
// 创建一个 OkHttpClient 实例,并配置日志拦截器和超时
private val httpClient = OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().apply {
// 打印请求体和响应体
level = HttpLoggingInterceptor.Level.BODY
}
)
.connectTimeout(15, TimeUnit.SECONDS) // 连接超时
.readTimeout(20, TimeUnit.SECONDS) // 读取超时
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(httpClient) // 设置自定义的 OkHttpClient
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
// 借助泛型实化,我们可以进一步优化上述代码
// 因为泛型 T 的类型信息在运行时依旧保留,在函数内部,我们可以直接访问 T 的 Class 对象 (T::class.java),无需手动传入 Class 对象。
inline fun <reified T> create(): T = create(T::class.java)
}
现在,获取一个 Service 接口的动态代理对象只需这样:
val appService = ServiceCreator.create(AppService::class.java)
// 或者
val appService = ServiceCreator.create<AppService>()