在 Android 中,我們常用 Retrofit 作為 HTTP client 來和後端的 RESTful APIs 溝通。Kotlin coroutine 可以讓 Retrofit 更加容易使用。本章將介紹如何搭配 coroutine 來使用 Retrofit。
Table of Contents
依賴引入
為了要可以使用 Retrofit 和 coroutine,我們必須要在專案的 build.gradle 裡加上以下的 dependencies。第一個是要讓專案可以使用 coroutine,另外兩個是為了可以使用 Retrofit。
dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
...
}Postman Echo API
我們將以 Postman 的 Echo API 作為虛擬的後端。Echo API 的 POST 請求 URL 為 https://postman-echo.com/post。我們將會送出以下的 JOSN 資料。
{
"name": "wayne",
"sex": 1
}Echo API 將會回傳以下的 JSON 資料。
{
"args": {},
"data": {
"name": "wayne",
"sex": 1
},
"files": {},
"form": {},
"headers": {
"x-forwarded-proto": "https",
"x-forwarded-port": "443",
"host": "postman-echo.com",
"x-amzn-trace-id": "Root=1-60af6a1c-4b2e2a28669e915137e9fed3",
"content-length": "37",
"content-type": "application/json",
"user-agent": "PostmanRuntime/7.28.0",
"accept": "*/*",
"cache-control": "no-cache",
"postman-token": "c59277d0-4d31-4b48-ab3c-8b0ab1eee9a6",
"accept-encoding": "gzip, deflate, br"
},
"json": {
"name": "wayne",
"sex": 1
},
"url": "https://postman-echo.com/post"
}Kotlin Coroutine
本文章中會使用到 Kotlin coroutine,如果你對 coroutine 還不熟悉的話,可以先參考以下的文章。
建立 Retrofit Service
新增 Service.kt,並在裡面宣告 interface Service。然後,宣告 post() 方法來對應 /post API。注意 post() 方法要宣告為 suspend。
package com.waynestalk.example
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface Service {
data class PostRequest(
val name: String,
val sex: Int,
)
data class PostResponse(
val data: PostRequest,
val json: PostRequest,
val headers: Map,
val url: String,
)
@POST("/post")
suspend fun post(@Body request: PostRequest): Response
} 再來新增 Server.kt,並宣告 object Server。在 Server 中,我們建立 Retrofit 實例,並設定後端的 URL 和轉換 JSON 字串成物件的 Gson converter 。這樣 Retrofit 的部分大致就完成。
package com.waynestalk.example
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object Server {
private const val URL = "https://postman-echo.com"
private val service: Service
init {
val client = OkHttpClient.Builder().build()
val retrofit = Retrofit.Builder()
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
service = retrofit.create(Service::class.java)
}
}結合 Coroutine 與 Retrofit
Retrofit 設定好後,再來就是要呼叫 Retrofit 來建立請求並取得資料。在 Server 中宣告 post() 方法來呼叫 Service.post()。因為 Server.post() 主要就是呼叫 Service.post() 並取得資料,所以我們希望它永遠不要在 main thread 中執行。因此,我們將整個方法包在 withContext(Dispatchers.IO) 中,這樣不管呼叫者在哪個 coroutine context,Server.post() 都會在 Dispatchers.IO 下執行。注意 Server.post() 也要宣告為 suspend 方法。
package com.waynestalk.example
import android.util.Log
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import kotlinx.coroutines.withContext
object Server {
...
private val tag = Server::class.java.name
suspend fun post(name: String, sex: Int): Pair = withContext(Dispatchers.IO) {
Log.d(tag, "Thread is ${Thread.currentThread().name}")
val request = Service.PostRequest(name, sex)
val response = service.post(request)
if (response.isSuccessful) {
val body = response.body()!!
return@withContext Pair(body.json.name, body.json.sex)
} else {
throw Exception(response.errorBody()?.charStream()?.readText())
}
}
} 呼叫 Server.post()
最後,讓我們來看看如何在 Activity 中呼叫 Server.post()。在以下的程式碼中,我們可以看到程式碼相當地簡潔。我們不需要先切換至 Dispatchers.IO 來呼叫 Server.post(),然後再切換至 Dispatchers.Main 來更新 UI。
另外,注意程式碼是以同步的方式呈現,但是執行起來卻是非同步的。Coroutine 讓程式碼可以更精簡而且更簡單。
class MainActivity : AppCompatActivity() {
private val tag = MainActivity::class.java.name
private lateinit var postButton: Button
private lateinit var nameTextView: TextView
private lateinit var ageTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
postButton = findViewById(R.id.postButton)
nameTextView = findViewById(R.id.nameTextView)
ageTextView = findViewById(R.id.ageTextView)
postButton.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val (name, sex) = Server.post("Wayne", 1)
Log.d(tag, "Thread is ${Thread.currentThread().name}")
nameTextView.text = name
ageTextView.text = if (sex == 1) "male" else "female"
}
}
}
}結論
搭配 Kotlin coroutine 使用 Retrofit 讓程式碼簡潔許多。Coroutine 的 withContext() 還可以讓一方法固定在指定的 context 下執行。這樣就不用怕開發者不小心在 main thread 中執行 IO。








