這篇開始,進入第三單元「Android 的架構」。在上個單元,我們雖說了要儘量用單元測試的方式,但其實要做起來還是有點困難的,這是因為Activity經常有著過多的邏輯,導至測試不易。
出版書:
Android TDD 測試驅動開發:從 UnitTest、TDD 到 DevOps 實踐
在第三單元,將介紹以下:
這篇首先要介紹的是MVP的架構,MVP將內容從呈現(Presenter)和資料處理(Model)與內容(View)分開。
在MVC的架構,通常會把layout(xml)當成View,Activity當成Controller。事實上,Activity 卻是Controller 與View的混合,於是Activity既要做處理View,也負責商業邏輯。使得Activity越來越肥。
MVC 與 MVP 的最大差異在於MVP把Activity的商業邏輯移到Presenter,Activity 專心於View
MVP:
範例:
這是一個商品的頁面,上面的資料是跟WebAPI取得商品資料(商品名稱、螢幕大小、售價)
建立ProductActivity 為MVP 中的View。
建立ProductContract ,裡面放了IProductView、IProductPresenter 2個Interface。
建立ProductPresenter,負責商業邏輯,與Model互動。
建立ProductRepository,負責取得商品資料。
首先是Model,也就是Repository,建立一個IPoroductRepository的Interface。
interface IProductRepository {
//傳入商品編號,取得商品資料
fun getProduct(productId: String, loadProductCallback: LoadProductCallback)
interface LoadProductCallback {
//回傳商品資料Response
fun onProductResult(productResponse: ProductResponse)
}
}
實作ProductRepository.getProduct
。ProductRepository的建購子傳入productAPI
,這是用來模擬API取得資料。
class ProductRepository(private val productAPI: IProductAPI) : IProductRepository {
override fun getProduct(productId: String, loadProductCallback: IProductRepository.LoadProductCallback) {
productAPI.getProduct(productId, object : IProductAPI.ProductDataCallback {
override fun onGetResult(productResponse: ProductResponse) {
loadProductCallback.onProductResult(productResponse)
}
})
}
}
新增ProductAPI
,用來模擬取得WebAPI的產品資料
interface IProductAPI {
interface ProductDataCallback {
fun onGetResult(productResponse: ProductResponse)
}
fun getProduct(productId:String, ProductDataCallback: ProductDataCallback)
}
class ProductAPI: IProductAPI {
override fun getProduct(productId:String, loadAPICallBack: IProductAPI.ProductDataCallback) {
//模擬從API取得資料
val handler = Handler()
handler.postDelayed(Runnable {
val productResponse = ProductResponse()
productResponse.id = "pixel3"
productResponse.name = "Google Pixel 3"
productResponse.desc = "5.5吋螢幕"
productResponse.price = 27000
callback.onGetResult(productResponse)
}, 1000)
}
}
商品資料的Model,這個Response就是用來將WebAPI回傳的資料存到這個DataModel
class ProductResponse {
lateinit var id: String
lateinit var name: String
lateinit var desc: String
var price: Int = 0
}
到目前為止,我們完成了MVP裡Model的部分,ProductRepository負責跟ServiceAPI取得商品資料
MVP的架構會有一個Contract的類別,裡面是定義View與Presenter之間的互動:
1.Activity 呼叫Presenter的Interface
2.Presenter callback的Interface
class ProductContract {
interface IProductPresenter {
//取得商品資料
fun getProduct(productId: String)
}
interface IProductView {
//取得資料的Callback
fun onGetResult(productResponse: ProductResponse)
}
}
Presenter 實作 IProductPresenter
,這裡的建構子必須傳入ProductContract.IProductView
,當Presenter跟Repository取得資料時,會呼叫ProductContract.IProductView.onGetResult
通知View通新畫面。
Activity負責2件事
1.跟Presenter要資料
2.實作IProductView.onProductResult 將商品Response放至UI上
1.跟Presenter要資料,在這個步驟,View必須將自已傳給ProductPresenter,讓ProductPresenter跟Repository取得資料後可以callback要求View顯示資料。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val productRepository = ProductRepository(ProductAPI())
// view必須將自已傳給Presenter,也就是this
val productPresenter = ProductPresenter(this, productRepository)
//向Presenter取得資料
productPresenter.getProduct(productId)
}
2.實作IProductView.onProductResult 將商品Response放至UI上
//實作IProductView.onGetResult
override fun onGetResult(productResponse: ProductResponse) {
//將商品Response放到View上
productName.text = productResponse.name
productDesc.text = productResponse.desc
val currencyFormat = NumberFormat.getCurrencyInstance()
currencyFormat.maximumFractionDigits = 0
val price = currencyFormat.format(productResponse.price)
productPrice.text = price
}
可以看到View被分割的很乾淨,只負責跟Presenter取資料、更新ProductResponse的資料到View
這樣MVP的架構就完成了,給大家一個練習,這個畫面下方有一個「購買」的按鈕。按下購買後,如購買成功Toast「購買成功」,購買失敗則Alert「購買失敗」應該怎麼寫。答案在範例下載。
下一篇將介紹MVP架構下的單元測試。
範例下載:
https://github.com/evanchen76/MVPUnitTestSample
給Android 初學者 的快速成長 線上課程
1️⃣ UI 進階實戰 — Material Design Component 讓你簡單做出效果超好的UI
2️⃣ 動畫入門到進階 — 用動畫提升使用者體驗
3️⃣ 架構設計 — MVP、MVVM 讓你程式碼好維護
1️⃣ + 2️⃣ + 3️⃣ 3堂組合包更划算 — Android 架構設計 + 動畫入門到進階 + UI 進階實戰