Android:使用 Retrofit 和 Kotlin Coroutine

Photo by Uliana Kopanytsia on Unsplash
Photo by Uliana Kopanytsia on Unsplash
在 Android 中,我們常用 Retrofit 作為 HTTP client 來和後端的 RESTful APIs 溝通。Kotlin coroutine 可以讓 Retrofit 更加容易使用。本章將介紹如何搭配 coroutine 來使用 Retrofit。

在 Android 中,我們常用 Retrofit 作為 HTTP client 來和後端的 RESTful APIs 溝通。Kotlin coroutine 可以讓 Retrofit 更加容易使用。本章將介紹如何搭配 coroutine 來使用 Retrofit。

完整程式碼可以在 下載。

依賴引入

為了要可以使用 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。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Gemma Evans on Unsplash
Read More

Android:Hilt 依賴注入

Hilt 是基於 Dagger 且設計在 Android 上使用的 dependency injection library。所以在開發 Android 時,使用 Hilt 會比使用 Dagger 更加地方便。本文章將介紹如何使用 Hilt。
Read More