今天來實戰,將前三天學習到關於 coroutine 的基本概念和用法在我們的專案內實作!
因為我們學到的是用 launch
啟動 coroutine
來向網路請求資料,所以想先從這邊開始下手,將目前的程式碼修改為以 launch
啟動 :
這是目前失敗的程式碼 :
向網路請求資料
private suspend fun fetchCoffeeShopData(): Deferred<Response> {
return CoroutineScope(Dispatchers.IO).async {
// 創建一個 OkHttpClient 實例
val client = OkHttpClient()
// 設置要發送的 HTTP 請求
val request = Request.Builder()
.url("http://cafenomad.tw/api/v1.2/cafes/taipei")
.build()
// 使用 OkHttpClient 發送同步請求
client.newCall(request).execute()
}
}
呼叫 fetchCoffeeShopData()
binding.button.setOnClickListener {
runBlocking {
try {
val response = fetchCoffeeShopData().await()
// 在主線程更新 UI,顯示回應內容
withContext(Dispatchers.Main) {
// 檢查回應是否成功
if (response.isSuccessful) {
val shops = response.body?.string()
// 這個 Scope 即是在 Android Main Thread 上執行,可以在這裡取得 Server 回傳的資料後更新 UI
binding.textView.text = shops
}
else {
// 處理請求失敗的情況
println("Request failed with code: ${response.code}")
}
}
}
catch (e: Exception) {
println("Error: ${e.message}")
}
}
}
修改方法的定義,改為無回傳值 :
// 原先的程式碼 :
private suspend fun fetchCoffeeShopData(): Deferred<Response> {}
// 修改後 :
private suspend fun fetchCoffeeShopData() {}
使用 withContext()
搭配 Dispatcher.IO
將目前的執行緒切換為工作執行緒 :
// 原先的程式碼
private suspend fun fetchCoffeeShopData(): Deferred<Response> {
return CoroutineScope(Dispatchers.IO).async {
}
}
// 修改後
private suspend fun fetchCoffeeShopData() {
withContext(Dispatchers.IO) {
}
}
自定義 Throwable
CoffeeShopsRefreshError
類別來拋出例外 :
// 修改後
class CoffeeShopsRefreshError(message: String, cause: Throwable?) : Throwable(message, cause)
使用 try-catch
做例外處理,包住發送網路請求的程式碼 :
// 原先程式碼
// 使用 OkHttpClient 發送同步請求
client.newCall(request).execute()
// 修改後
val response = try {
// 使用 OkHttpClient 發送同步請求
client.newCall(request).execute()
}
catch (cause: Throwable) {
throw CoffeeShopsRefreshError("Unable to refresh data", cause)
}
將回傳結果 response strirng 使用 LiveData
通知訂閱者(觀察者)來更新畫面
// 修改後
if (response.isSuccessful) {
// 成功後通知畫面更新
liveShops.postValue(response.body?.string())
}
else {
throw CoffeeShopsRefreshError("Unable to refresh data", null)
}
來修改接收使用者點擊事件後需要執行的部分,這邊會先透過 Dispatcher.Main
讓 coroutine
在主執行緒被啟動,而且也要記得 suspend 函式
需要在 coroutine
內被呼叫 :
// 原先程式碼
binding.button.setOnClickListener {
runBlocking {
}
}
// 修改後
binding.button.setOnClickListener {
// 這個 Scope 即是在 Android Main Thread 上執行向網路取得咖啡廳資料
CoroutineScope(Dispatchers.Main).launch {
}
}
修改執行網路請求的方法
// 原先的程式碼
try {
val response = fetchCoffeeShopData().await()
}
catch (e: Exception) {
println("Error: ${e.message}")
}
這邊有多加 progressbar,在請求前會出現,當請求結束後會消失 :
try {
binding.progressbar.visibility = View.VISIBLE
fetchCoffeeShopData()
}
catch (e: CoffeeShopsRefreshError) {
binding.progressbar.visibility = View.INVISIBLE
binding.textView.text = "Request failed \nmessage: ${e.message}"
}
// 原先的程式碼
// 在主線程更新 UI,顯示回應內容
withContext(Dispatchers.Main) {
// 檢查回應是否成功
if (response.isSuccessful) {
val shops = response.body?.string()
// 這個 Scope 即是在 Android Main Thread 上執行,可以在這裡取得 Server 回傳的資料後更新 UI
binding.textView.text = shops
}
else {
// 處理請求失敗的情況
println("Request failed with code: ${response.code}")
}
}
這裡我將畫面更新的程式碼拉出來,因為是透過 launch
啟動 coroutine
,表示我們需要特別處理回傳資料,可以使用 callback
也可以使用今天介紹的 LiveData
。
LiveData
實作了觀察者模式,所以只要被觀察的對象資料有異動,就會通知觀察者,我們只需要實作觀察的方法,就可以直接在數據更新時刷新畫面。
// 修改後
private val liveShops = MutableLiveData<String?>()
// 更新畫面資料
liveShops.observe(this) { coffeeShops ->
CoroutineScope(Dispatchers.Main).launch {
binding.textView.text = coffeeShops
binding.progressbar.visibility = View.INVISIBLE
}
}
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val liveShops = MutableLiveData<String?>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Hello Kotlin
binding.textView.text = "Hello Kotlin"
// 點擊按鈕後取得咖啡廳資料
binding.button.setOnClickListener {
// 這個 Scope 即是在 Android Main Thread 上執行向網路取得咖啡廳資料
CoroutineScope(Dispatchers.Main).launch {
try {
binding.progressbar.visibility = View.VISIBLE
fetchCoffeeShopData()
}
catch (e: CoffeeShopsRefreshError) {
binding.progressbar.visibility = View.INVISIBLE
binding.textView.text = "Request failed \nmessage: ${e.message}"
}
}
}
// 更新畫面資料
liveShops.observe(this) { coffeeShops ->
CoroutineScope(Dispatchers.Main).launch {
binding.textView.text = coffeeShops
binding.progressbar.visibility = View.INVISIBLE
}
}
}
private suspend fun fetchCoffeeShopData() {
withContext(Dispatchers.IO) {
// 創建一個 OkHttpClient 實例
val client = OkHttpClient()
// 設置要發送的 HTTP 請求
val request = Request.Builder()
.url("http://cafenomad.tw/api/v1.2/cafes/taipei")
.build()
val response = try {
// 使用 OkHttpClient 發送同步請求
client.newCall(request).execute()
}
catch (cause: Throwable) {
throw CoffeeShopsRefreshError("Unable to refresh data", cause)
}
if (response.isSuccessful) {
// 成功後通知畫面更新
liveShops.postValue(response.body?.string())
}
else {
throw CoffeeShopsRefreshError("Unable to refresh data", null)
}
}
}
}
class CoffeeShopsRefreshError(message: String, cause: Throwable?) : Throwable(message, cause)
還在看要怎麼上傳影片,等我研究研究在更新上來!
今天使用 launch
改寫失敗的程式碼,雖然成功了,但並不是我理想中的樣子,因為更新畫面的部分並沒有想像中的直觀,就算不用 coroutine
也能達到差不多的效果。所以下一篇要來改用 async
啟動 coroutine
,到時候再來看看是不是想要的結果吧~