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
对象和errorCode
和errorMsg
组成的,然后data
对象又包括了datas
数组,所以定义解析类的过程应该是先定义PassageResponse
类,里面存放一个名为data
的PassageData
对象,然后PassageData
里面存放名为datas
的Passage
数组(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>
}
|
这样我们添加的user
和token
两个参数,并用@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) }
|
我们用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()) .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() 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}:)-->>"); } } } }
|
这样就能使用协程很方便的完成网络请求了。