這個MVP的寫法是否更好一點呢?
本文主要討論如何將Android中的 Presenter
以一種簡潔的方式做到與 View
的解耦,並且不容易脫軌(變的混亂)。本文假設 頁面資料
完全是由 Presenter
管理。。
我們先來看一下常規的 Presenter
與 View
的寫法(下文對於 Presenter
與 View
的敘述簡稱為 VP
),並探討一下這種寫法存在什麼問題:
常規的寫法
對於Android中的 VP
我們為了做到互相解耦,我們通常要給 Presenter
定義一個介面,給 View
定義一個介面, 假設我們要寫一個搜尋邏輯,可能會寫出如下程式碼:
- 定義介面
class SearchProtocol{ interface Presenter{ fun search() //搜尋 } interface View { fun showSearchResult() //顯示搜尋結果 } } 複製程式碼
- 介面實現
class SearchPresenter : SearchProtocol.Presenter{ } class SearchView : SearchProtocol.View{ val presenter:SearchProtocol.Presenter = SearchPresenterImpl1() fun doSearch(){ presenter.search() } overried showSearchResult(){} } 複製程式碼
我認為這樣寫是存在一些問題的:
問題一 : 介面過多
PV
還沒開始寫,兩個介面先定義下來了。(雖然做到了 PV
一定意義上的解耦)
問題二 : View依賴於固定的Presenter介面
比如大家經常使用的一種構建UI的方式 : 一個 RecyclerView
構建所有UI,頁面不同的部分使用不同的 RecyclerView
的 Item
來表現。
假如下圖這個搜尋結果頁就是使用 RecyclerView
構建的:

如果使用者點選篩選按鈕(其實本質還是搜尋),那麼就需要呼叫 persenter.search()
。但是篩選這個item實際上是使用 RecyclerView
的一個 ItemView
構建的,因此我可能就需要把 presenter(SearchPresenter)
的例項傳到這個ItemView,ItemView在篩選時呼叫 presenter.search()
這樣做可能有一些不好的地方:
-
View
依賴了一個固定的Presenter
介面,VP
存在耦合,不利於複用。如果在其他的介面我想複用這個ItemView,那麼傳另一個介面的Presenter
很明顯是不合適的。 -
不利於
View
的單元測試。其實RecyclerView
中的ItemView
也是一個View
,如果在例項化這個View
的時候還需要傳一個指定的Presenter(SearchPresenter)
,那麼單元測試這個View
時為了提供它的環境就有點麻煩了,因為還要關心Presenter
例項。 -
對於資料狀態的獲取
Presenter
也需要提供給View
一個方法。Presenter
的介面很容易變的越來越多。
那怎麼寫可以解決上面的問題呢?我認為下面是一種可行的方案:
更純淨的VP寫法
對於 VP
, 我認為他們之間的交流可以分為兩種:
-
View
接收使用者事件,觸發Presenter
執行一些邏輯,比如資料載入。 -
View
需要獲取當前的資料狀態,來決定UI
的展現或者UI
層的一些邏輯,比如事件打點。
描述上面兩種交流方式,可以把 Presenter
抽象為下面這個介面:
open class Action() open class State() abstract class BasePresenter(){ abstract fun dispatch(action:Action) abstract fun <T : State> queryStatus(statusClass: KClass<T>): T? } 複製程式碼
Action
: View
觸發的操作,可以通過一個 Action
來通知 Presenter
。
State
: 描述 View
可以從Presenter中獲得的資料的狀態。
BasePresenter
: View
只依賴這個最抽象的介面。通過 Action
和 State
來與 Presenter
互動。
下面詳細來解釋一下 Action
和 State
的思想:
使用Action統一Presenter的處理邏輯
在往下閱讀之前可以先看一下這篇文章 : segmentfault.com/a/119000000… 這篇文章介紹了redux的設計思想,而下文所要介紹的Presenter的實現就是借鑑了Redux的設計思想。
對於常規的寫法, Presenter
的處理邏輯是通過呼叫固定的方法實現的,這就導致依賴於一個固定的Presenter介面, 參考Redux的設計,可以這樣設計Presenter:
class Action class BasePresenter{ abstract fun dispatch(action: Action) } 複製程式碼
即所有的 Presenter
都實現這一個介面,外界對於 Presenter
邏輯的觸發都通過 dispatch()
方法實現,對於上面搜尋那個例子可以這樣實現:
class SearchAction(val keyword:String) : Action class SearchPresenter(searchView:SearchViewProtocol):BasePresenter{ overried fun dispatch(action:Action){ when(action){ //只處理感興趣的action is SearchAction -> doSearch() } } fun doSearch(){ //... searchView.showSearchResult() } } class SearchView:SearchViewProtocol{ val presenter:BasePresenter = SearchPresenter(this) fun doSearch(){ presenter.dispatch(SearchAction("narato")) } ...... } 複製程式碼
這樣寫後對比於常規的寫法有什麼好處呢?
- 減少了
Presneter
介面的定義,由於現在Presenter
對外層的抽象是dispatch
方法,因此新的VP不需要特別定義與View
配套的Presenter
介面。 -
View
不依賴於固定的Presenter
介面,統一使用BasePresenter
,View可以很好的複用和進行單元測試。 -
View
發出的Action
,Presenter
可以選擇處理,也可以不處理。
View使用 State
來獲取當前的資料狀態
在Redux中, View dispatch Action
後對於資料的變化,可以通過訂閱(觀察)資料來重新整理UI。不過對於這次我介紹的 VP
, View
的資料是由 Presenter
所提供的,那麼就不能使用Redux這種方法了(View不會直接接觸資料)。
舉一個例子,比如有一個自定義按鈕,它是否可以點選執行一些事情,依賴於當前介面某些資料的狀態。這個狀態並不屬於當前 View
那常規我們可能會這樣做:
//View中的按鈕被點選 class MyBtton(presenter:SearchPresenter){ fun onClick(){ if(presenter.canExecute()){ } } } 複製程式碼
如果這樣寫那就又會出現上面的問題:
- 依賴具體的presenter,複用困難
- 單元測試麻煩
- 為獲取狀態,又多了一個方法
我們可以借用 dispatch
的設計,引入 State
:
class SeachState class SeachBasePresenter{ fun <T : SeachState> queryState(statteClass: KClass<T>): T? } 複製程式碼
即我們可以這樣實現這個需求:
class MyBtton(presenter:SeachBasePresenter){ fun onClick(){ if(presenter.queryState(MyButtonState::class)?.canExecute == true){ } } } class MyButtonState(val canExecute:Boolean = false) : SearchState class SeachButtonPresenter{ override fun <T : SearchState> queryStatus(statusClass: KClass<T>): T? { return when (statusClass) { MyButtonState::class -> { MyButtonState(true) as T } else -> null } } } 複製程式碼
這樣的做法不僅解決了上面的問題。並且 SearchState
是一個物件,我們可以封裝許多資料的狀態,減少 State
的定義。
上面只是我應用在目前業務中的一種 PV
寫法,當然對於不同的業務,可能這套寫法會出現問題,歡迎討論。
歡迎關注我的 Android進階計劃 看更多幹貨
歡迎關注我的微信公眾號:susion隨心
