iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0

這篇是設定給已經有 MVC 架構概念的讀者,因為後面想使用 MVVM 作為 APP 的設計架構,為了能無痛轉移,所以從概念比較相似的 MVP 開始著手~

在專案內無論是使用何種設計架構,都是為了不讓程式碼越來越髒,養肥 Activity,所以筆者認為挑個自己喜歡的學習,熟悉後再學習其他的架構,一次學太多可能反而會適得其反,將架構用的千奇百怪。

MVP 是 model、view、presenter 的縮寫。為了改善 MVC 的缺點,所以將 controller 替換成 presenter,在 MVC 的概念裡,xml 會做為 view,而 activity 作為 controller,但是在開發時會發現,Activity 會負責到 UI 邏輯和商業邏輯,根本是 view 和 controller 的混合。所以在 MVP 中,特地將商業邏輯抽出來給 presenter 處理,讓 Activity 專心和使用者互動。

MVP 之間的職責 :

  • Model - 負責管理和操作資料。包含取得本地資料庫、向網路請求資料和 SharedPreferences 等等與資料相關的部分。
  • View - 透過 UI 和使用者互動,接收由使用者傳遞的訊息、資料有異動時負責更新畫面
  • Presenter - 負責業務邏輯,並處理接收從 view 傳來的使用者操作事件。使用 model 提供的方法來處理資料;與 MVC 不同的是,當 model 操作完資料後,不會直接傳給 view,而是回傳給 presenter ,透過 presenter 通知 view 更新畫面。

優點 : 從 MVC 三者的相互依賴,改為只依賴 presenter,分工也較明確、方便單元測試。

缺點 : 隨著不斷的開發,presenter 會越來越肥。

重構1 - 抽出 Model

新增 MainModel.kt

  • 新增 Package - model

    d14_1.png

  • 新增 MainModel

    d14_2.png

新增 MainContract.kt

MVP 大多是透過介面來互動,所以我們需要先來製作 MainContract,裡面會有三個 interface :

  • Model interface
  • View interface
  • Presenter

先來看一下 Kotlin 是如何實作介面 :

class Child : MyInterface {
    override fun bar() {
        // body
    }
}

應用,這邊我用 alter + enter 讓 IDE 自動產生 :

d14_3.png

接著建立 MainContract.Model interface :

d14_4.png

MainContract :

interface MainContract {

    interface Model {

    }
}

事前準備差不多了,現在移動到 MainActivity~~

重構2 - 抽出 Presenter

實例化 model

待會要把 iModel 傳給 Presenter 持有,這樣 Presenter 就可以呼叫 model 的方法 :

val iModel = MainModel()

建立 MainPresenter

這次直接在 Activity 建立 MainContract.Presenter :

private lateinit var iPresenter : MainContract.Presenter

d14_5.png

目前的 MainContract :

interface MainContract {

    interface Model {

    }

    interface Presenter {

    }
}

接著將 presenter 實例化,將 iModel 作為參數傳入建構子 :

iPresenter = MainPresenter(iModel)

d14_6.png

目前的 MainPresenter :

class MainPresenter(iModel: MainModel) : MainContract.Presenter {

}

建立 getCoffeeShops()

來建立 presenter 取得咖啡廳資料的方法,會有個 city 的參數 :

val city = ""
iPresenter.getCoffeeShops(city)

d14_7.png

MainContract :

interface MainContract {

    interface Model {

    }

    interface Presenter {
        
        fun getCoffeeShops(city: String) {
            TODO("Not yet implemented")
        }
    }
}

MainPresenter 實作 :

接著移動到 MainPresenter.kt

  • 初始化 iModel

    class MainPresenter(iModel: MainModel) : MainContract.Presenter {
    
        val iModel: MainContract.Model
    
        init {
    
            this.iModel = iModel
        }
    }
    
  • 覆寫 getCoffeeShops()

    d14_8.png

    override fun getCoffeeShops(city: String) {
    
    }
    
  • 透過 iModel 呼叫 API

    先建立 BaseContract.BaseCallback<T> :

    d14_9.png

    建立好 BaseContract.BaseCallback<T> 後直接在裡面加入失敗和成功的 callback :

    interface BaseContract {
    
        interface BaseCallback<T> {
    
            fun onFail(errMsg: String)
            fun onSuccess(t : T)
        }
    }
    

    MainPresenter :

    override fun getCoffeeShops(city: String) {
    
        iModel.getCoffeeShops(city, object : BaseContract.BaseCallback<String?> {
            override fun onFail(errMsg: String) {
                TODO("Not yet implemented")
            }
    
            override fun onSuccess(t: String?) {
                TODO("Not yet implemented")
            }
        })
    }
    

    接著 在 MainContract 內加入方法 :

    d14_10.png

    MainContract :

    interface MainContract {
    
        interface Model {
    
            fun getCoffeeShops(city: String, callback: BaseContract.BaseCallback<String?>) {
    
            }
        }
    
        interface Presenter {
    
            fun getCoffeeShops(city: String) {
    
            }
        }
    }
    
  • 調整建構子 - 加入 View 的持有

    其實是忘記加入 View 了,而且還發現傳入的 model 型別錯了,來調整一下吧!

    按下 ctrl + F6

    d14_11.png

    調整好後,把 MainContract.View 也建一下 :

    d14_12.png

    現在的 MainContract.kt

    interface MainContract {
    
        interface Model {
    
            fun getCoffeeShops(city: String, callback: BaseContract.BaseCallback<String?>) {
    
            }
        }
    
        interface Presenter {
    
            fun getCoffeeShops(city: String) {
    
            }
        }
    
        interface View {
    
        }
    }
    

    修正好的 MainPresenter.kt

    class MainPresenter(iModel: MainContract.Model, iView: MainContract.View) : MainContract.Presenter {
    
        val iModel: MainContract.Model
        val iView: MainContract.View
    
        init {
            this.iModel = iModel
            this.iView = iView
        }
    }
    
  • 完成 getCoffeeShops() :

    1. onFail(errMsg: String) 內讓 view 執行 showFail(errMsg) 來顯示錯誤訊息在畫面上
    2. onSuccess(shops: String?) 內讓 view 執行 showCoffeeShop(shops) 顯示咖啡廳資訊在畫面上
    override fun getCoffeeShops(city: String) {
    
        iModel.getCoffeeShops(city, object : BaseContract.BaseCallback<String?> {
            override fun onFail(errMsg: String) {
    
                iView.showFail(errMsg)
            }
    
            override fun onSuccess(shops: String?) {
    
                iView.showCoffeeShop(shops)
            }
    
  • 建立 View 的 showFail(errMsg)

    d14_13.png

  • 建立 View 的 showCoffeeShop(shops)

    d14_14.png

現在的 MainContract.kt

剛剛順便調整了一下參數的型別,讓他可以吃 NULL

interface MainContract {

    interface Model {

        fun getCoffeeShops(city: String?, callback: BaseContract.BaseCallback<String?>) {

        }
    }

    interface Presenter {

        fun getCoffeeShops(city: String?) {

        }
    }

    interface View {

        fun showFail(errMsg: String?) {

        }

        fun showCoffeeShop(shops: String?) {

        }
    }
}

這樣 MainPresenter 大致都完成了~~~

馬上來進入 MainModel~~~

重構3 - MainModel 實作

接著到 MainModel.kt 將方法實作出來 :

d14_15.png

使用 WithContext(Dispatcher.IO) 切換到 IO 執行緒,IDE 就警告我們要把方法修正為 suspend,因為筆者前面忘記加了~

d14_16.png

MainPresenter.kt 也要一起調整為 suspend 函式:

override suspend fun getCoffeeShops(city: String?) {

    iModel.getCoffeeShops(city, object : BaseContract.BaseCallback<String?> {
        override fun onFail(errMsg: String?) {

            iView.showFail(errMsg)
        }

        override fun onSuccess(shops: String?) {

            iView.showCoffeeShop(shops)
        }
    })
}

MainContract.kt 也會調整 :

interface MainContract {

    interface Model {

        suspend fun getCoffeeShops(city: String?, callback: BaseContract.BaseCallback<String?>) {

        }
    }

    interface Presenter {

        suspend fun getCoffeeShops(city: String?) {

        }
    }

		...
}

將原本在 MainActivity 的程式碼複製貼上,在修改為 callback 來回傳 API 執行結果 :

override suspend fun getCoffeeShops(city: String?, callback: BaseContract.BaseCallback<String?>) {

    withContext(Dispatchers.IO) {

        // 創建一個 OkHttpClient 實例
        val client = OkHttpClient()

        // 設置要發送的 HTTP 請求
        val request = Request.Builder()
            .url("http://cafenomad.tw/api/v1.2/cafes/$city")
            .build()

        val response = try {

            // 使用 OkHttpClient 發送同步請求
            client.newCall(request).execute()
        }
        catch (cause: IOException) {

            callback.onFail(cause.message)
            return@withContext
        }

        if (response.isSuccessful) {

            // 成功後通知 presenter
            callback.onSuccess(response.body?.string())
        }
        else {

            callback.onFail("Unable to refresh data")
        }
    }
}

重構4 - View 的實作

好的~到目前 Model 也準備 OK,最後來把 View (MainActivity) 調整一下 :

先實作 MainContract.View :

class MainActivity : AppCompatActivity(), MainContract.View {

還有他的兩個方法 :

override fun showFail(errMsg: String?) {

    runOnUiThread {
        binding.textView.text = "Request failed \nmessage: ${errMsg}"
        binding.progressbar.visibility = View.GONE
    }
}

override fun showCoffeeShop(shops: String?) {

    runOnUiThread {
        binding.textView.text = shops
        binding.progressbar.visibility = View.GONE
    }
}

最後是按鈕點擊 :

binding.button.setOnClickListener {

    lifecycleScope.launch(Dispatchers.Main) {

        binding.progressbar.visibility = View.VISIBLE

        iPresenter.getCoffeeShops("taipei")
    }
}

重構結束拉~~ 來運行一下

d14_ 18.png

終於~~~今天這篇好長,辛苦看到這邊的大家!

今日推推

Yes


上一篇
Day13 使用 Async 啟動 Coroutine 取得網路請求 (下)
下一篇
Day15 解析 Json 字串
系列文
喝咖啡要30天?一起用 Kotlin 打造尋找好喝咖啡的 App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言