Skip to content

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等解析库一同下载下来。

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

json
{
  "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的信息:

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

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

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

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

kotlin
interface AppService {

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

}

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


activity_main.xml中:

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中:

kotlin
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()传入的Callback对象中。与Okhttp()不同的是,Retrofit会在内部自动开启子线程,然后在回调到Callback时会自己切换回主线程,我们不用去关注线程问题。在onRespnse()方法中,调用response.body()方法就会得到解析后的banner对象。

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

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
<?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数据:

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对象和errorCodeerrorMsg组成的,然后data对象又包括了datas数组,所以定义解析类的过程应该是先定义PassageResponse类,里面存放一个名为dataPassageData对象,然后PassageData里面存放名为datasPassage数组(Passage对象则是存放文章的相关信息)。然后AppService.kt中:

kotlin
interface AppService {

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

}

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

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

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

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

kotlin
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请求时,接口地址如下:

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

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

kotlin
interface AppService {

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

}

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

kotlin
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注解进行声明:

kotlin
interface AppService {

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

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

kotlin
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单例类:

kotlin
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时就很简单了:

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

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

kotlin
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接口的动态代理对象就可以了:

kotlin
val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object  : Callback<banner> {
    ...
}

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

与Rxjava结合使用

build.gradle中添加Rxjava的依赖:

gradle
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:

kotlin
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适配器:

kotlin
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)
}

然后进行网络请求:

kotlin
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

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

导入依赖:

kotlin
    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接口:

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

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

创建Retrofit构建器:

kotlin
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)

}

然后定义网络仓库层:

kotlin
object BannerNetWork {
    val appService = AppService.create<API>()

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

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

然后ViewModel里面:

kotlin
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中:

kotlin
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}:)-->>");
            }
        }
    }
}

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