本文主要討論如何將Android中的 Presenter 以一種簡潔的方式做到與View的解耦,即View只依賴于最抽象的Presenter接口, 而不是具體的Presenter接口。
常規(guī)的寫法
對于Android中的VP我們?yōu)榱俗龅交ハ嘟怦?,我們通常要給Presenter定義一個接口,給View定義一個接口, 假設(shè)我們要寫一個搜索邏輯,可能會寫出如下代碼:
- 定義接口
class SearchProtocol{
interface Presenter{
fun search() //搜索
}
interface View {
fun showSearchResult() //顯示搜索結(jié)果
}
}
- 接口實(shí)現(xiàn)
class SearchPresenter : SearchProtocol.Presenter{ }
class SearchView : SearchProtocol.View{
val presenter:SearchProtocol.Presenter = LoginPresenter()
fun doSearch(){
presenter.search()
}
overried showSearchResult(){}
}
這樣寫有什么問題呢 ?
VP還沒開始寫,兩個接口先定義下來了
對于某些例子, 會導(dǎo)致View依賴于Presenter
比如說現(xiàn)在大家經(jīng)常使用的一種構(gòu)建UI的方式:一個RecyclerView構(gòu)建所有UI,假如下圖這個搜索結(jié)果頁就是使用RecyclerView構(gòu)建的:

如果用戶點(diǎn)擊篩選按鈕(其實(shí)本質(zhì)還是搜索),那么就需要調(diào)用 persenter.search()。但是篩選這個item實(shí)際上是使用RecyclerView的一個Item構(gòu)建的,因此我可能就需要把presenter傳到這個ItemView,ItemView在篩選時調(diào)用presenter.search()
這樣做有什么不好呢?:
依賴了一個固定的presenter接口,不利于復(fù)用,如果在其他的界面我想復(fù)用這個ItemView,那么傳另一個界面的Presenter很明顯是不合適的。
不利于單元測試: 其實(shí)RecyclerView中的ItemView也是一個View,如果在實(shí)例化這個View的時候還需要傳一個指定的Presenter,那么單元測試這個View時為了提供它的環(huán)境就有點(diǎn)麻煩了。
更純凈的VP寫法
統(tǒng)一Presenter的處理邏輯
在往下閱讀之前可以先看一下這篇文章 : https://segmentfault.com/a/1190000008736866
這篇文章介紹了redux的設(shè)計思想,而下文所要介紹的Presenter的新實(shí)現(xiàn)就是借鑒了Redux的設(shè)計思想。
對于常規(guī)的寫法,Presenter的處理邏輯是通過調(diào)用固定的方法實(shí)現(xiàn)的,這就導(dǎo)致依賴于一個固定的Presenter接口, 參考Redux的設(shè)計,我們可以這樣設(shè)計Presenter:
class Action
class BasePresenter{
abstract fun dispatch(action: Action)
}
即所有的Presenter都實(shí)現(xiàn)這一個接口,外界對于Presenter邏輯的觸發(fā)都通過dispatch()方法實(shí)現(xiàn),對于上面搜索那個例子可以這樣實(shí)現(xiàn):
class SearchAction(val keyword:String):Action
class SearchPresenter(searchView:SearchViewProtocol):BasePresenter{
overried fun dispatch(action:Action){
when(action){
is SearchAction -> doSearch()
}
}
fun doSearch(){
//...
searchView.showSearchResult()
}
}
class SearchView:SearchViewProtocol{
val presenter:BasePresenter = SearchPresenter(this)
fun doSearch(){
presenter.dispatch(SearchAction("narato"))
}
......
}
這樣寫后對比于常規(guī)的寫法有什么好處呢?
- 減少了Presneter接口的定義,由于現(xiàn)在Presenter對外層的抽象是
dispatch方法,因此新的VP不需要特定定義與View配套的Presenter接口。 - View不依賴于固定的Presenter接口,統(tǒng)一使用BasePresenter,View可以很好的復(fù)用和進(jìn)行單元測試。
- View發(fā)出的Action,Presenter可以選擇處理,也可以不處理。
View對于狀態(tài)的獲取
在Redux中,View dispatch Action后對于數(shù)據(jù)的變化,可以通過訂閱(觀察)數(shù)據(jù)來刷新UI。不過對于這次我介紹的VP,View的數(shù)據(jù)是由Presenter所提供的,那么就不能使用Redux這種方法了。
其實(shí)在Android中,對于VP,我們 認(rèn)為且應(yīng)該 :View所需要的數(shù)據(jù)應(yīng)該在presenter刷新UI時由Presenter傳遞過來, 比如:
presenter.showSearchResult(result)
即,View只負(fù)責(zé)展示UI,不應(yīng)有其他邏輯。上面這種方式在一定程度上可以使View完成自己的職責(zé),但在一些情況下就有問題了:
比如有一個按鈕,它是否可以點(diǎn)擊執(zhí)行一些事情,依賴于當(dāng)前界面某些數(shù)據(jù)的狀態(tài)。
那常規(guī)我們可能會這樣做:
class MyBtton(presenter:Presenter){
fun onClick(){
if(presenter.canExecute()){
}
}
}
如果這樣寫那就又會出現(xiàn)上面的問題:
- 依賴具體的presenter,復(fù)用困難
- 單元測試麻煩
為了達(dá)到 view完全依賴抽象的Presenter 我們可以借用dispatch的設(shè)計:
class SeachState
class SeachBasePresenter{
fun <T : SeachState> queryState(statteClass: KClass<T>): T?
}
即我們可以這樣實(shí)現(xiàn)這個需求:
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
}
}
}
這樣做依舊是達(dá)到了View只依賴于抽象的SearchBasePresenter的目的,不依賴于具體的Presenter,解決了上面的問題。
總結(jié)
因此我們在設(shè)計VP結(jié)構(gòu)時可以設(shè)計成這種結(jié)構(gòu),可以達(dá)到View完全依賴于抽象的Presenter,保證程序在正確的軌道上發(fā)展:
open class Action()
open class State()
abstract class BasePresenter() {
abstract fun dispatch(action:Action)
abstract fun <T : State> queryStatus(statusClass: KClass<T>): T?
}
歡迎關(guān)注我的Android進(jìn)階計劃