iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Kotlin

喝咖啡要30天?一起用 Kotlin 打造尋找好喝咖啡的 App系列 第 11

Day11 二戰 Coroutine ! 使用 OKHttp 串接全台咖啡廳資料的 API-5

  • 分享至 

  • xImage
  •  

今天來實戰,將前三天學習到關於 coroutine 的基本概念和用法在我們的專案內實作!

使用 launch 搭配 LiveData

因為我們學到的是用 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}")
            }
        }
    }
    

修改網路請求的部分

  1. 修改方法的定義,改為無回傳值 :

    // 原先的程式碼 : 
    private suspend fun fetchCoffeeShopData(): Deferred<Response> {}
    
    // 修改後 : 
    private suspend fun fetchCoffeeShopData() {}
    
  2. 使用 withContext()搭配 Dispatcher.IO 將目前的執行緒切換為工作執行緒 :

    // 原先的程式碼
    private suspend fun fetchCoffeeShopData(): Deferred<Response> {
    
    		return CoroutineScope(Dispatchers.IO).async {
    
    		}
    }
    
    // 修改後
    private suspend fun fetchCoffeeShopData() {
    
        withContext(Dispatchers.IO) {
    
        }
    }
    
  3. 自定義 Throwable CoffeeShopsRefreshError 類別來拋出例外 :

    // 修改後
    class CoffeeShopsRefreshError(message: String, cause: Throwable?) : Throwable(message, cause)
    
  4. 使用 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)
    }
    
  5. 將回傳結果 response strirng 使用 LiveData 通知訂閱者(觀察者)來更新畫面

    // 修改後
    if (response.isSuccessful) {
    
        // 成功後通知畫面更新
        liveShops.postValue(response.body?.string())
    }
    else {
    
        throw CoffeeShopsRefreshError("Unable to refresh data", null)
    }
    

修改點擊按鈕後的程式碼

  1. 來修改接收使用者點擊事件後需要執行的部分,這邊會先透過 Dispatcher.Maincoroutine 在主執行緒被啟動,而且也要記得 suspend 函式需要在 coroutine 內被呼叫 :

    // 原先程式碼
    binding.button.setOnClickListener {
    
        runBlocking {
    
        }
    }
    
    // 修改後
    binding.button.setOnClickListener {
    
        // 這個 Scope 即是在 Android Main Thread 上執行向網路取得咖啡廳資料
        CoroutineScope(Dispatchers.Main).launch {
    
        }
    }
    
  2. 修改執行網路請求的方法

    // 原先的程式碼
    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}"
    }
    

3. 加入 LiveData 來更新畫面

// 原先的程式碼

// 在主線程更新 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,到時候再來看看是不是想要的結果吧~

今日推推

Yes


上一篇
Day10 實作 Google Codelab Coroutine - 3
下一篇
Day12 使用 Async 啟動 Coroutine 取得網路請求 (上)
系列文
喝咖啡要30天?一起用 Kotlin 打造尋找好喝咖啡的 App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言