這篇是設定給已經有 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 之間的職責 :
優點 : 從 MVC 三者的相互依賴,改為只依賴 presenter,分工也較明確、方便單元測試。
缺點 : 隨著不斷的開發,presenter 會越來越肥。
新增 Package - model
新增 MainModel
MVP 大多是透過介面來互動,所以我們需要先來製作 MainContract,裡面會有三個 interface :
先來看一下 Kotlin 是如何實作介面 :
class Child : MyInterface {
override fun bar() {
// body
}
}
應用,這邊我用 alter + enter
讓 IDE 自動產生 :
接著建立 MainContract.Model interface :
MainContract :
interface MainContract {
interface Model {
}
}
事前準備差不多了,現在移動到 MainActivity~~
待會要把 iModel 傳給 Presenter 持有,這樣 Presenter 就可以呼叫 model 的方法 :
val iModel = MainModel()
這次直接在 Activity 建立 MainContract.Presenter :
private lateinit var iPresenter : MainContract.Presenter
目前的 MainContract :
interface MainContract {
interface Model {
}
interface Presenter {
}
}
接著將 presenter 實例化,將 iModel 作為參數傳入建構子 :
iPresenter = MainPresenter(iModel)
目前的 MainPresenter :
class MainPresenter(iModel: MainModel) : MainContract.Presenter {
}
getCoffeeShops()
來建立 presenter 取得咖啡廳資料的方法,會有個 city 的參數 :
val city = ""
iPresenter.getCoffeeShops(city)
MainContract :
interface MainContract {
interface Model {
}
interface Presenter {
fun getCoffeeShops(city: String) {
TODO("Not yet implemented")
}
}
}
接著移動到 MainPresenter.kt
初始化 iModel
class MainPresenter(iModel: MainModel) : MainContract.Presenter {
val iModel: MainContract.Model
init {
this.iModel = iModel
}
}
覆寫 getCoffeeShops
()
override fun getCoffeeShops(city: String) {
}
透過 iModel 呼叫 API
先建立 BaseContract.BaseCallback<T>
:
建立好 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 內加入方法 :
MainContract :
interface MainContract {
interface Model {
fun getCoffeeShops(city: String, callback: BaseContract.BaseCallback<String?>) {
}
}
interface Presenter {
fun getCoffeeShops(city: String) {
}
}
}
調整建構子 - 加入 View 的持有
其實是忘記加入 View 了,而且還發現傳入的 model 型別錯了,來調整一下吧!
按下 ctrl + F6
調整好後,把 MainContract.View 也建一下 :
現在的 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()
:
onFail(errMsg: String)
內讓 view 執行 showFail(errMsg)
來顯示錯誤訊息在畫面上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)
建立 View 的 showCoffeeShop(shops)
現在的 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~~~
接著到 MainModel.kt 將方法實作出來 :
使用 WithContext(Dispatcher.IO) 切換到 IO 執行緒,IDE 就警告我們要把方法修正為 suspend,因為筆者前面忘記加了~
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")
}
}
}
好的~到目前 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")
}
}
重構結束拉~~ 來運行一下
終於~~~今天這篇好長,辛苦看到這邊的大家!