Retrofit

Retrofit是一个十分好用的网络库,是基于Okhttp的基础进一步开发出来的应用层网络通信库。

基本用法

一款应用程序中发起的网络请求大多是指向同一个服务器域名的,同时我们也不怎么关心网络通信的具体细节。Retrofit则基于上面的情况等,只需要配置根路径,然后在指定服务器接口地址时只需要用相对地址就好了。同时Retrofit允许我们对服务器接口归类,将一类的接口定义到一个接口文件中,使代码结构更合理。


我们先导入Retrofit依赖库,在build.gradle文件中添加:

1
2
3
4
5
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
...
}

Retrofit库是基于Okhttp开发的,第一条会将相关的库自动导入,无需我们手动引入,第二条是Retrofit的转换库,会将Gson等解析库一同下载下来。

然后我们以玩安卓bannerAPI为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"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的信息:

1
data class bannerInfo(val desc: String, val url: String, val title: String)

我们选取了其中一些字段存放,然后我们观察这个json实际上是里面有一个数组,所以我们新建一个banner.kt用来作为解析后的数据(与Gson方式相同):

1
data class banner(val data: List<bannerInfo>, val errorCode: Int, val errorMsg: String)

然后我们定义一个接口文件AppService:

1
2
3
4
5
6
interface AppService {

@GET("banner/json")
fun getAppData(): Call<banner>

}

我们定义了一个getAppData()方法,并添加了一条@GET()注解,里面是相对路径,意思是调用这个方法时会发起一条GET请求。然后将方法的返回值设为Call类型,然后通过泛型指定将数据转换成什么对象。我们这里就传banner,如果是一段对象数组,就传List<对象名>就可以了。然后我们开始使用这个方法。


activity_main.xml中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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()传入的$\texttt{Callback}$对象中。与Okhttp()不同的是,Retrofit会在内部自动开启子线程,然后在回调到Callback时会自己切换回主线程,我们不用去关注线程问题。在onRespnse()方法中,调用response.body()方法就会得到解析后的banner对象。

然后同样要在AndroidManifest.xml中申请权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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内容:

1
2
3
4
<?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数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"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对象和errorCodeerrorMsg组成的,然后data对象又包括了datas数组,所以定义解析类的过程应该是先定义PassageResponse类,里面存放一个名为dataPassageData对象,然后PassageData里面存放名为datasPassage数组(Passage对象则是存放文章的相关信息)。然后AppService.kt中:

1
2
3
4
5
6
interface AppService {

@GET("article/list/{page}/json")
fun getPage(@Path("page") page: Int): Call<PassageResponse>

}

使用@GET注解时使用了个{page}占位符,在getPage()中添加了一个$\texttt{page}$参数,并用@Path("page")注解来申明这个参数。通过这样我们可以自定义$\texttt{page}$的值来得到请求地址。

另外,在请求时通常会让我们传入一系列参数,比如:

1
http://example.com/get_data.json?u=<user>&t=<token>

这时候我们如果还用@Path注解的话就会很麻烦,而Retrofit对这种带参数的请求提供了一种语法支持:

1
2
3
4
5
6
interface AppService {

@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Passage>

}

这样我们添加的usertoken两个参数,并用@Query注解进行声明,其中的u和t即网络请求要求传入的参数的名称。


另外,网络请求不止GET这一种类型,还有POST,PUT,PATCH,DELETE这几种,POST用于提交数据,PUT,PATCH用于修改数据,DELETE用于删除数据。用Retrofit时也支持了@POST,@PUT,@PATCH,@DELETE这几种注解用于请求。

比如我们在进行POST请求时,接口地址如下:

1
2
/data/create
{"id" : 1, "content" : "Datas"}

我们需要将需要提交的数据放到请求的body部分,可以使用@Body注解完成:

1
2
3
4
5
6
interface AppService {

@POST("data/create")
fun createData(@Body data: Data): Call<ResponseBody>

}

我们传入了一个Data类型的参数用于存放提交的数据并用了@Body注解:

1
data class Data(val id: String, val content: String)

上面代码中的Call<ResponseBody>什么意思呢,因为通常POST下来的数据我们并不关心,而ResponseBody则表示接受任意的数据,但不会对数据进行解析。

有时候,服务器还会要求我们用在HTTP请求的header中指定参数,比如:

1
2
3
/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0

我们可以用Retrofit中的@Headers注解进行声明:

1
2
3
4
5
6
7
interface AppService {

@Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
@GET("get_data.json")
fun getHeaderData(): Call<Data>

}

但这样只能进行静态的header声明,如果需要动态申明的话就要用@Header注解:

1
2
3
4
5
6
interface AppService {

@GET("get_data.json")
fun getHeaderData(@Header("User-Agent") userAgent: String, @Header("Cache-Control") cacheControl: String): Call<Data>

}

这样就能在请求时将数据传到Header中了。

Retrofit构建器的最佳写法

我们可以将Retrofit的构建和调用的create()方法创建动态代理对象封装起来,就可以简化很大一部分代码量了。创建一个ServiceCreator单例类:

1
2
3
4
5
6
7
8
9
10
11
12
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时就很简单了:

1
2
3
4
val appService = ServiceCreator.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback<banner> {
...
}

因为JVM的泛型擦除机制,而内联函数可以直接进行内容替换。因此我们可以通过内联函数进行泛型实化进一步简化:

1
2
3
4
5
6
7
8
9
10
11
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接口的动态代理对象就可以了:

1
2
3
4
val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<banner> {
...
}

这样就可以很方便的进行网络请求了。

与Rxjava结合使用

build.gradle中添加Rxjava的依赖:

1
2
3
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:

1
2
3
4
5
6
7
8
9
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适配器:

1
2
3
4
5
6
7
8
9
10
11
12
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)
}

然后进行网络请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

协程能模拟在单线程上使用多线程进行任务,能使异步代码看着像同步代码,我们可以通过这些来很方便的进行网络请求。

导入依赖:

1
2
3
4
5
6
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接口:

1
2
3
4
interface API {
@GET("banner/json")
suspend fun getBanner(): ResponseResult<List<Banner>>
}

Retrofit现在原生就支持了协程,只需要声明成suspend关键字就可以了,返回值也可以直接设置成ResponseResult数据类,而不是Call回调。

创建Retrofit构建器:

1
2
3
4
5
6
7
8
9
10
11
12
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)

}

然后定义网络仓库层:

1
2
3
4
5
object BannerNetWork {
val appService = AppService.create<API>()

suspend fun getBanner() = appService.getBanner()
}

注意这里也要声明成挂起函数。

然后ViewModel里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}:)-->>");
}
}
}
}

这样就能使用协程很方便的完成网络请求了。