在 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<String, String>, val url: String, ) @POST("/post") suspend fun post(@Body request: PostRequest): Response<PostResponse> }
再來新增 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<String, Int> = 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。