Retrofit
Retrofit
是一个十分好用的网络库,是基于Okhttp
的基础进一步开发出来的应用层网络通信库。
基本用法
一款应用程序中发起的网络请求大多是指向同一个服务器域名的,同时我们也不怎么关心网络通信的具体细节。Retrofit则基于上面的情况等,只需要配置根路径,然后在指定服务器接口地址时只需要用相对地址就好了。同时Retrofit允许我们对服务器接口归类,将一类的接口定义到一个接口文件中,使代码结构更合理。
我们先导入Retrofit依赖库,在build.gradle
文件中添加:
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
...
}
Retrofit
库是基于Okhttp
开发的,第一条会将相关的库自动导入,无需我们手动引入,第二条是Retrofit
的转换库,会将Gson
等解析库一同下载下来。
{
"data": [
{
"desc": "我们支持订阅啦~",
"id": 30,
"imagePath": "https://www.wanandroid.com/blogimgs/42da12d8-de56-4439-b40c-eab66c227a4b.png",
"isVisible": 1,
"order": 2,
"title": "我们支持订阅啦~",
"type": 0,
"url": "https://www.wanandroid.com/blog/show/3352"
},
{
"desc": "",
"id": 6,
"imagePath": "https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
"isVisible": 1,
"order": 1,
"title": "我们新增了一个常用导航Tab~",
"type": 1,
"url": "https://www.wanandroid.com/navi"
},
{
"desc": "一起来做个App吧",
"id": 10,
"imagePath": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"isVisible": 1,
"order": 1,
"title": "一起来做个App吧",
"type": 1,
"url": "https://www.wanandroid.com/blog/show/2"
}
],
"errorCode": 0,
"errorMsg": ""
}
我们新建一个bannerInfo
的数据类用来存放每个banner的信息:
data class bannerInfo(val desc: String, val url: String, val title: String)
我们选取了其中一些字段存放,然后我们观察这个json实际上是里面有一个数组,所以我们新建一个banner.kt
用来作为解析后的数据(与Gson
方式相同):
data class banner(val data: List<bannerInfo>, val errorCode: Int, val errorMsg: String)
然后我们定义一个接口文件AppService
:
interface AppService {
@GET("banner/json")
fun getAppData(): Call<banner>
}
我们定义了一个getAppData()
方法,并添加了一条@GET()
注解,里面是相对路径,意思是调用这个方法时会发起一条GET请求。然后将方法的返回值设为Call
类型,然后通过泛型指定将数据转换成什么对象。我们这里就传banner
,如果是一段对象数组,就传List<对象名>
就可以了。然后我们开始使用这个方法。
在activity_main.xml
中:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
设置了一个按钮用于启动网络请求,一个TextView用于展示内容。然后在MainActivity.kt
中:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.button.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback<banner> {
override fun onFailure(call: Call<banner>, t: Throwable) {
Log.d("zzx",t.message.toString());
}
override fun onResponse(call: Call<banner>, response: Response<banner>) {
val list = response.body()
if(list != null) {
binding.textView.text = list.data[0].title
}
}
})
}
}
}
在按钮的点击事件中,先构建了一个Retrofit
对象,baseUrl()
用于指定请求的根路径,addConverterFactory()
用于指定Retrofit
解析数据时用的转换库,这里用的是GsonConverterFactory.create()
。然后我们调用create()
方法,传入Service
接口所对应的Class
类型,创建该接口的动态代理对象,可以随意调用接口中定义的所有方法。
上述代码中,当我们调用这个接口的getAppData()
方法时,会返回一个Call<banner>
对象,我们再调用enqueue()
方法,就会根据注解中的内容去进行网络请求了,响应的数据会回调到enqueue()
传入的Okhttp()
不同的是,Retrofit
会在内部自动开启子线程,然后在回调到Callback
时会自己切换回主线程,我们不用去关注线程问题。在onRespnse()方法
中,调用response.body()
方法就会得到解析后的banner对象。
然后同样要在AndroidManifest.xml
中申请权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 申请权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:networkSecurityConfig="@xml/net_config"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RetrofitTest"
tools:targetApi="31">
...
</application>
</manifest>
xml/net_config.xml
内容:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true"/>
</network-security-config>
然后当我们点击按钮时,就会显示出我们成功网络请求的内容了。
处理复杂的接口地址类型
比如玩Android的首页文章列表这个API,地址:https://www.wanandroid.com/article/list/0/json,这里面有个`0`这个页码参数,当我们想要传入不同的页码时又当怎么办呢?先看他请求出来的json数据:
{
"data": {
"curPage": 1,
"datas": [
{
"adminAdd": false,
"apkLink": "",
"audit": 1,
"author": "张鸿洋",
"canEdit": false,
"chapterId": 543,
"chapterName": "Android技术周报",
"collect": false,
"courseId": 13,
"desc": "",
"descMd": "",
"envelopePic": "",
"fresh": false,
"host": "",
"id": 29494,
"isAdminAdd": false,
"link": "https://www.wanandroid.com/blog/show/3745",
"niceDate": "1天前",
"niceShareDate": "1天前",
"origin": "",
"prefix": "",
"projectLink": "",
"publishTime": 1740153600000,
"realSuperChapterId": 542,
"selfVisible": 0,
"shareDate": 1740154200000,
"shareUser": "",
"superChapterId": 543,
"superChapterName": "技术周报",
"tags": [],
"title": "Android 技术周刊 (2025-02-15 ~ 2025-02-22)",
"type": 0,
"userId": -1,
"visible": 1,
"zan": 0
},
{
"adminAdd": false,
"apkLink": "",
"audit": 1,
"author": "",
"canEdit": false,
"chapterId": 502,
"chapterName": "自助",
"collect": false,
"courseId": 13,
"desc": "",
"descMd": "",
"envelopePic": "",
"fresh": false,
"host": "",
"id": 29493,
"isAdminAdd": false,
"link": "https://juejin.cn/post/7471630643534512164",
"niceDate": "2天前",
"niceShareDate": "2天前",
"origin": "",
"prefix": "",
"projectLink": "",
"publishTime": 1740131039000,
"realSuperChapterId": 493,
"selfVisible": 0,
"shareDate": 1740131039000,
"shareUser": "ldlywt",
"superChapterId": 494,
"superChapterName": "广场Tab",
"tags": [],
"title": "我写了个App,上架 Google Play 一年,下载不到 10 次,于是决定把它开源了",
"type": 0,
"userId": 2470,
"visible": 1,
"zan": 0
}
]
},
"errorCode": 0,
"errorMsg": ""
}
我们观察这段数据,是由data
对象和errorCode
和errorMsg
组成的,然后data
对象又包括了datas
数组,所以定义解析类的过程应该是先定义PassageResponse
类,里面存放一个名为data
的PassageData
对象,然后PassageData
里面存放名为datas
的Passage
数组(Passage
对象则是存放文章的相关信息)。然后AppService.kt
中:
interface AppService {
@GET("article/list/{page}/json")
fun getPage(@Path("page") page: Int): Call<PassageResponse>
}
使用@GET
注解时使用了个{page}
占位符,在getPage()
中添加了一个@Path("page")
注解来申明这个参数。通过这样我们可以自定义
另外,在请求时通常会让我们传入一系列参数,比如:
http://example.com/get_data.json?u=<user>&t=<token>
这时候我们如果还用@Path
注解的话就会很麻烦,而Retrofit
对这种带参数的请求提供了一种语法支持:
interface AppService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Passage>
}
这样我们添加的user
和token
两个参数,并用@Query
注解进行声明,其中的u和t即网络请求要求传入的参数的名称。
另外,网络请求不止GET这一种类型,还有POST,PUT,PATCH,DELETE
这几种,POST
用于提交数据,PUT,PATCH
用于修改数据,DELETE
用于删除数据。用Retrofit
时也支持了@POST,@PUT,@PATCH,@DELETE
这几种注解用于请求。
比如我们在进行POST
请求时,接口地址如下:
/data/create
{"id" : 1, "content" : "Datas"}
我们需要将需要提交的数据放到请求的body
部分,可以使用@Body
注解完成:
interface AppService {
@POST("data/create")
fun createData(@Body data: Data): Call<ResponseBody>
}
我们传入了一个Data
类型的参数用于存放提交的数据并用了@Body
注解:
data class Data(val id: String, val content: String)
上面代码中的Call<ResponseBody>
什么意思呢,因为通常POST
下来的数据我们并不关心,而ResponseBody
则表示接受任意的数据,但不会对数据进行解析。
有时候,服务器还会要求我们用在HTTP
请求的header
中指定参数,比如:
/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0
我们可以用Retrofit
中的@Headers
注解进行声明:
interface AppService {
@Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
@GET("get_data.json")
fun getHeaderData(): Call<Data>
}
但这样只能进行静态的header
声明,如果需要动态申明的话就要用@Header
注解:
interface AppService {
@GET("get_data.json")
fun getHeaderData(@Header("User-Agent") userAgent: String, @Header("Cache-Control") cacheControl: String): Call<Data>
}
这样就能在请求时将数据传到Header
中了。
在进行类似于登录时提交username,password这种表单提交方法,应使用
@FormUrlEncoded
和@Field("username")
,而不是@Body
kotlin@FormUrlEncoded @POST("/user/login") fun login(@Field("username") username: String, @Field("password") password: String): Observable<PersonalInfo>
Retrofit构建器的最佳写法
我们可以将Retrofit
的构建和调用的create()
方法创建动态代理对象封装起来,就可以简化很大一部分代码量了。创建一个ServiceCreator
单例类:
object ServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
}
然后在我们调用Retrofit
获取AppService
时就很简单了:
val appService = ServiceCreator.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback<banner> {
...
}
因为JVM的泛型擦除机制,而内联函数可以直接进行内容替换。因此我们可以通过内联函数进行泛型实化进一步简化:
object ServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> create(): T = retrofit.create(T::class.java) //就可以直接返回AppService的实例
}
我们用inline
关键字修饰方法,用reified
关键字修饰泛型,然后我们就可以用泛型来获取AppService
接口的动态代理对象就可以了:
val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<banner> {
...
}
这样就可以很方便的进行网络请求了。
与Rxjava结合使用
在build.gradle
中添加Rxjava
的依赖:
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
在AppService.kt
中更改返回类型为Observable
:
interface AppService {
@GET("banner/json")
fun getAppData(): Observable<banner>
@GET("article/list/{page}/json")
fun getPage(@Path("page") page: Int): Observable<PassageResponse>
}
然后在Retrofit
实例ServiceCreator.kt
中添加Rxjava
适配器:
object ServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create()) //添加Rxjava适配器
.build()
inline fun <reified T> create(): T = retrofit.create(T::class.java)
}
然后进行网络请求:
class MainActivity : AppCompatActivity() {
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.button.setOnClickListener {
val appService = ServiceCreator.create<AppService>()
val disposable = appService.getPage(0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ passageResponse ->
//成功处理
Toast.makeText(this, passageResponse.data.datas[0].title,Toast.LENGTH_SHORT).show()
},{ error ->
//失败处理
Toast.makeText(this,error.message,Toast.LENGTH_SHORT).show()
})
compositeDisposable.add(disposable)
}
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.dispose() //防止内存泄漏
}
}
与协程结合使用
https://github.com/generalio/AndroidStudy/tree/main/CoroutinesNetWork
协程能模拟在单线程上使用多线程进行任务,能使异步代码看着像同步代码,我们可以通过这些来很方便的进行网络请求。
导入依赖:
implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("androidx.activity:activity-ktx:1.10.1")
创建API接口:
interface API {
@GET("banner/json")
suspend fun getBanner(): ResponseResult<List<Banner>>
}
Retrofit现在原生就支持了协程,只需要声明成suspend
关键字就可以了,返回值也可以直接设置成ResponseResult
数据类,而不是Call回调。
创建Retrofit构建器:
object AppService {
private const val BASE_URL = "https://www.wanandroid.com/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> create(): T = retrofit.create(T::class.java)
}
然后定义网络仓库层:
object BannerNetWork {
val appService = AppService.create<API>()
suspend fun getBanner() = appService.getBanner()
}
注意这里也要声明成挂起函数。
然后ViewModel
里面:
class BannerViewModel: ViewModel() {
val bannerViewModel: LiveData<List<Banner>> get() = _bannerLiveData
private val _bannerLiveData = MutableLiveData<List<Banner>>()
fun getBanner() {
viewModelScope.launch {
val res = try {
BannerNetWork.getBanner()
}catch (e: Exception) {
null
}
if(res != null && res.errorCode == 0 && res.data != null) {
_bannerLiveData.postValue(res.data)
}
}
}
}
这里的viewModelScope
来自于androidx.lifecycle:lifecycle-viewmodel-ktx
,为ViewModel
提供了一个CoroutineScope
,会在ViewModel
清除时自动取消,否则就需要用CoroutineScope()
,然后手动创建Job()
并管理生命周期。
这里launch
启动后,发起了网络请求会自动调用IO线程,然后在请求完成后返回数据后又会自动切换到主线程上,我们只需要设置liveData
就好了。
然后MainActivity
中:
class MainActivity : AppCompatActivity() {
private val viewModel: BannerViewModel by viewModels() //使用的activit-ktx能快速设置ViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getBanner()
viewModel.bannerViewModel.observe(this){banners ->
if(banners != null) {
Log.d("zzx","(${banners[0].title}:)-->>");
}
}
}
}
这样就能使用协程很方便的完成网络请求了。