昨天學到了如何同步執行向 server 取得全台咖啡廳資料,來複習一下,同步是程式碼會按照我們撰寫的順序依序執行,在閱讀上是十分直觀的。那大家可以猜到非同步執行了嗎~ 沒錯,非同步表示程式碼並不會按照順序跑,在閱讀上相對不容易。那在什麼情況下會需要使用到非同步執行呢? 讓我們來想像若生活上沒有非同步執行會發生什麼事~
我是全聯的店員,整間店只有我一位,需要負責補貨、拉排面、整理環境和結帳等等。在執行結帳的時候,原本只有2~3位客人,突然結帳的隊伍暴增,但因為只有我一位店員,只能繼續努力的結帳。阿,送貨的司機來了,可是還沒辦法收貨,只能請司機加入等待的隊伍,等我將這排長長的結帳隊伍處理完畢,才能執行收貨。
接著來看看使用非同步的全聯會如何運作。
我是全聯的店員,但這間店的員工除了我之外還有A 同事和 B 同事,好開心。今天幫客人結帳的時候突然排隊的人暴增,但沒關係,只要呼叫 “請支援收銀”,我的 AB 同事就會過來幫忙,他們幫忙結完帳後還會通知我結束了,好開心。
看完狀況劇大家有比較理解在程式上的兩種執行方式嗎? 當主執行緒在執行非同步任務時,會請其他執行緒幫忙,在等待期間,主執行緒可以繼續去做原本在做的,或是去處理其他的事情,等非同步的執行結果出來後,幫忙的執行緒會將執行結果通知給主執行緒,告訴它我做完了。常用於執行較耗時的操作例如上傳下載,或是不曉得什麼時候可以取得執行結果像是和 server 請求資料時,都可以透過非同步執行做處理,在使用者體驗上也會比較好,用戶就不需要為了等待執行結果而卡住不能做其他事。
為了不讓主執行緒在執行耗時的任務阻塞,那我們來試著建立新的執行緒處理取得咖啡廳的資料,就像是高速公路因為上下班導致交流道壅塞,而加開交流道是一樣的道理。
在 Kotlin 我們可以透過 thread{}
建立執行緒來請他幫忙做事,但 OKHttp 它有自己寫好的非同步呼叫,只要將原本的 execute()
改為 enqueue()
即可。
💡 情境 : 點擊按鈕 → 使用非同步執行取得咖啡廳資料 → callback 接收到回傳結果 → 在 UiThread
內刷新畫面。
這是目前的 getCoffeeShops()
:
private fun getCoffeeShops() {
// 創建一個單線程執行緒池
val service = Executors.newSingleThreadExecutor()
// 提交任務到執行緒池
service.submit {
// 創建一個 OkHttpClient 實例
val client = OkHttpClient()
// 設置要發送的 HTTP 請求
val request = Request.Builder()
.url("http://cafenomad.tw/api/v1.2/cafes/taipei")
.build()
// 在主線程中執行 OKHttp 請求(請注意這是同步操作,會阻塞主線程)
try {
// 使用 OkHttpClient 發送同步請求
val response = client.newCall(request).execute()
// 檢查回應是否成功
if (response.isSuccessful) {
val responseBody = response.body?.string()
// 在主線程更新 UI,顯示回應內容
runOnUiThread {
binding.textView.text = responseBody
}
}
else {
// 處理請求失敗的情況
println("Request failed with code: ${response.code}")
}
// 關閉執行緒池
service.shutdown()
}
catch (e: Exception) {
e.printStackTrace()
}
}
}
修改 getCoffeeShops()
方法,並使用 OKHttp
的非同步執行,且透過 Callback
取得咖啡廳資料,最後顯示在 TextView
。
將原先的 getCoffeeShops()
使用 Lamda 的方式加入參數 Callback
private fun getCoffeeShopsAsync(callback: (String?) -> Unit) {
}
這段程式碼的意思是我有一個 getCoffeeShopsAsync()
方法,方法有一個參數 callback
,他吃的參數型別為 String?
,Unit
表示沒有回傳值。
將 OKHttp 改用非同步執行的 enqueue()
// 使用 OkHttpClient 發送非同步請求
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
callback.invoke(e.message)
}
override fun onResponse(call: Call, response: Response) {
if(!response.isSuccessful) {
callback.invoke("fail! response code = ${response.code} \nmessage = ${response.message}")
}
else {
val responseBody = response.body?.string()
callback.invoke(responseBody)
}
}
})
這邊的 enqueue()
傳入了一個 Callback
,並且覆寫兩個方法: onFailure()
、onResponse()
我們在 onFailure()
處理呼叫 API 失敗的狀況;在 onResponse()
接收回傳的資料,接著透過從方法傳入的 callback 將結果拋出去。
刷新畫面的 TextView
// 取得咖啡廳資料
binding.button.setOnClickListener {
getCoffeeShopsAsync { shops ->
runOnUiThread {
binding.textView.text = shops
}
}
}
要注意畫面的刷新必須在 runOnUiThread {}
內處理,因為我們在呼叫 API 時已經切換到別條的執行緒了,若沒有在 UiThread
更新畫面肯定會報錯~~~
原本以為今天可以讀到 Coroutine,結果太高估自己了呀~~~ 只好明天見拉各位 ❤️
累到什麼都唸不出來….明天見拉大家~~