Android TDD 系列 — 17 Android MVP 架構

Evan Chen
Evan Android Note
Published in
10 min readOct 5, 2019

這篇開始,進入第三單元「Android 的架構」。在上個單元,我們雖說了要儘量用單元測試的方式,但其實要做起來還是有點困難的,這是因為Activity經常有著過多的邏輯,導至測試不易。

在第三單元,將介紹以下:

  • MVP的架構及單元測試。
  • MVVM的架構及寫單元測試。
  • APP加上呼叫WebAPI取得資料的功能、非同步的議題。

這篇首先要介紹的是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:

  • Model — 管理資料來源。例:SharedPreferences、Room、呼叫API
  • View — 顯示UI和與使用者互動I,如 Activity、Fragment
  • Presenter — 負責邏輯處理

範例:
這是一個商品的頁面,上面的資料是跟WebAPI取得商品資料(商品名稱、螢幕大小、售價)

建立ProductActivity 為MVP 中的View。
建立ProductContract ,裡面放了IProductView、IProductPresenter 2個Interface。
建立ProductPresenter,負責商業邏輯,與Model互動。
建立ProductRepository,負責取得商品資料。

Model

首先是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取得商品資料

Contract ( Interface)

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

Presenter 實作 IProductPresenter,這裡的建構子必須傳入ProductContract.IProductView,當Presenter跟Repository取得資料時,會呼叫ProductContract.IProductView.onGetResult通知View通新畫面。

View(Activity)

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

MVP、MVVM 影片教學,學習更快!

我開設了一門教MVP、MVVM 架構的線上課程,搭配Android Architecture Components。透過線上課程學習,效果更好!

👇點下方連結有medium優惠價👇
Android 架構設計 | 用 Architecture Components 打造易維護、可測試的App

👇另有3堂課一起的組合包更划算👇
Android 架構設計 + 動畫入門到進階 + UI 進階實戰

Android TDD 系列
下一篇:18 Android MVP 架構的單元測試

--

--