寫在前面
Android端的MVP架構已經出來有很長時間了。而對於Android的MVP實現模式,也並沒有個標準的實現方式。
現在市面上最流行的是google開源出來的一套MVP模型,此模型可到此google家MVP開源地址進行檢視。
而此篇部落格將要介紹的並不是google的MVP模型。而是根據我自身理解所建立的一種MVP模型。與google的MVP模型相比,此種MVP模型具有以下一些優勢:
- 支援單頁面繫結多個Presenter進行使用,便於進行Presenter複用
- 對Presenter進行生命週期派發,
- 自動對Presenter進行View的繫結與解綁。
- 剔除契約類Contract,避免類爆炸
MVP概念說明
- Model: 資料提供層,負責向Presenter提供資料或者提供資料處理入口
- View: 檢視層,負責接收Presenter通知,進行介面更新
- Presenter: View與Model的樞紐層,負責接收View層的命令,從Model層讀取並處理好資料後,通知View進行介面更新。
僅僅靠上面的文字來進行分層說明略顯空洞,所以這裡我們來通過一個簡單的sample程式碼
來做MVP分層概念說明, 如下方是個簡單的登入頁面的MVP實現:
interface LoginView:MVPView {
fun onLoginSuccess()
fun onLoginFailed()
}
class LoginPresenter(view:DemoView):MVPPresenter<DemoView>(view) {
fun login(username:String, password:String) {
LoginApis.login(username, password, object Callback {
override fun onSuccess() {
view.onLoginSuccess()
}
override fun onFailed() {
view.onLoginFailed()
}
})
}
}
class LoginActivity:BaseMVPActivity(),LoginView {
// 建立與繫結Presenter。
val presenter = LoginPresenter(this)
override fun createPresenters() = arrayOf(presenter)
override fun onLoginSuccess() {
// 接收資料請求任務的返回資料並展示
EasyToast.DEFAULT.show("登入成功")
}
override fun onLoginFailed() {
// 接收資料請求任務的返回資料並展示
EasyToast.DEFAULT.show("登入成功")
}
...
// 點選登入
fun onLoginClick() {
val username = ...
val password = ...
presenter.login(username, password)// 發起login任務請求
}
}
複製程式碼
1. LoginView
interface LoginView:MVPView {
fun onLoginSuccess()
fun onLoginFailed()
}
複製程式碼
繼承並擴充套件MVPView介面。很多人喜歡直接將此類作為MVP中的V層
,但是實際上,我更願意稱此為通訊協議介面
,作用是由V層
提供給P層
進行P-V繫結
,用於在P層
中通知V層
進行介面更新,類似於提供了一個Callback給P層進行使用
2. LoginActivity
class LoginActivity:BaseMVPActivity(),LoginView {
override fun onLoginSuccess() {...}
override fun onLoginFailed() {...}
// 發起login任務請求
fun onLoginClick() {presenter.login(username, password)}
}
複製程式碼
真正的View
層。可以是Activity
, Fragment
等。是真正進行介面更新的地方。
View
層需要持有Presenter
的物件,用於在需要的時候使用presenter
發起具體的資料請求處理任務
,比如上例中點選進行登入時。通過presenter
發起了登入任務, 並通過LoginView
協議介面,接收任務處理回撥
進行介面更新
3. LoginApis
LoginApis.login(username, password, object Callback {
override fun onSuccess() { view.onLoginSuccess() }
override fun onFailed() { view.onLoginFailed() }
})
複製程式碼
Model
層,也叫資料提供層。
與其他的MVP
不同,這裡並沒有要求Model
層需要定義一個特殊的介面去進行實現。所有的功能性api
。均可被視作為Model
層。比如這裡LoginApis
,便是用於提供登入模組的網路任務入口
。
Model
層與Presenter
層的通訊方式主要分為兩種:一種直接從Model層同步獲取
到指定資料,另一種是通過非同步回撥
的方式獲取到指定資料,即此處LoginApis
的通訊方式。
請注意避免直接向Model
層傳遞Presenter
去進行資料獲取。保證Model
的獨立性
4. LoginPresenter
Presenter
層,連線V-M的中間樞紐層。複雜的資料業務邏輯都在這裡進行處理。
結合上方示例:一個完整的M-P-V通訊流程,可分為以下幾步:
- 由
V層向P層
發起具體的處理任務 P層
接收到V層
發起的任務。呼叫M層
的api,獲取到原始資料
P層
對從M層
獲取到的原始資料
進行預處理(本示例因為較簡單,故沒有這一步)。- 將
處理完畢後
的資料。通過V層
提供的協議介面,通知到V層
去進行介面更新。
MVP實現
通過上面的例項與講解。相信可以使大家對MVP模型有一定的瞭解了,下面我們將一步步的介紹整個MVP模型框架的搭建
1. 基礎通訊協議介面定義:MVPView
interface MVPView {
fun getHostActivity():Activity
fun showLoadingDialog()
fun hideLoadingDialog()
fun toastMessage(message:String)
fun toastMessage(resId:Int)
}
複製程式碼
MVPView
中定義了一些基礎的協議方法。這些方法是所有V層
都需要的功能。比如Toast展示
、進行非同步任務時的載入中Dialog展示
等。
2. 基礎Presenter建立
open class MVPPresenter<T:MVPView>(private var view:T?){
fun attach(t:T) {
this.view = t
}
fun detach() {
this.view = null
}
fun isViewAttached() = view != null
fun getActivity() = view?.getHostActivity()?:throw RuntimeException("Could not call getActivity if the View is not attached")
// Lifecycle delegate
open fun onCreate(bundle: Bundle?) {}
...
open fun onDestroy(){}
}
複製程式碼
我們來一條條的捋一下:
首先。我們在上面的MVP說明中說過,MVPView
為協議介面,提供出來用於進行P-V繫結
,所以相應的就會有P-V的繫結與解綁功能
(為了方便使用,這裡也讓預設構造器自動進行了P-V繫結)
fun attach(t:T) { this.view = t }
fun detach() { this.view = null }
複製程式碼
然後,我們會經常需要在P層中使用繫結的Activity去進行各種操作。而p層
是不建議去持有Context例項
的,所以在此提供一個getActivity
方法,從繫結的view
中去獲取正確的Activity例項
提供使用:
fun getActivity() = view?.getActivity()?:throw RuntimeException("Could not call getActivity if the View is not attached")
複製程式碼
而isViewAttached
方法,則是專門為非同步回撥任務
所設計的api。因為如果是使用非同步回撥的方式
去從model層獲取的資料。那麼很可能,接收到回撥訊息的之前,這個時候view已被解綁置空
。導致通知失敗
所以。一般來說。對於任務中有非同步回撥操作
的,應該在回撥處。先行判斷是否已解綁
。若已解綁則跳過當前操作:
if (!isViewAttached()) return
view?.hideLoadingDialog()
複製程式碼
最後,則是提供的一堆onXXX
生命週期方法了。用於與activity/fragment 中的生命週期進行繫結。
3. V-P聯結器
與其他的MVP
相比不同,這裡提供MVPDispatcher
作為V-P聯結器
:
class MVPDispatcher{
private val presenters:MutableList<MVPPresenter<*>> = mutableListOf()
// ==== 新增與移除Presenter ========
fun <V:MVPView> addPresenter(presenter:MVPPresenter<V>) {...}
internal fun <V:MVPView> removePresenter(presenter:MVPPresenter<V>) {...}
// ==== 繫結生命週期 ==========
fun dispatchOnCreate(bundle:Bundle?) {...}
...
fun dispatchOnRestoreInstanceState(savedInstanceState: Bundle?) {...}
}
複製程式碼
可以看到此聯結器幹了以下幾件事:
addPresenter
與removePresenter
:向容器presenters
中新增或移除presenter例項
。 做到對單頁面繫結多個presenter的效果。dispatchOnXXX
:通過已新增的presenter
進行生命週期通知
. 做到V-P生命週期繫結的效果- 在接收到V層
destroy
銷燬通知時,自動移除解綁所有的presenter例項
。
fun dispatchOnDestroy() {
presenters.forEach {
if (it.isViewAttached()) { it.onDestroy() }
// 生命方法派發完畢後。自動解綁
removePresenter(it)
}
}
複製程式碼
4. BaseMVPActivity的建立
最後就是真正的V層
的建立了:Activity/Fragment
的MVP基類搭建!
這裡使用BaseMVPActivity
作為舉例說明。fragment
的基類搭建與此大同小異。
首先,我們需要確定BaseMVPActivity
所需要盡到的職責:
1. 一個具體的V層,需要持有一個唯一的MVPDispatcher進行操作
abstract class BaseMVPActivity:Activity() {
val mvpDispatcher = MVPDispatcher()
}
複製程式碼
2. 統一實現預設協議方法:MVPView
abstract class BaseMVPActivity:Activity(), MVPView {
...
override fun getHostActivity():Activity {...}
override fun showLoadingDialog() {...}
override fun hideLoadingDialog() {...}
override fun toastMessage(message:String) {...}
override fun toastMessage(resId:Int) {...}
}
複製程式碼
3. 藉助MVPDispatcher實現V-P,一對多的繫結
abstract class BaseMVPActivity:Activity(), MVPView {
...
// 由子類提供當前頁面所有需要繫結的Presenter。
open fun createPresenters():Array<out MVPPresenter<*>>? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
// 建立所有的presenter例項,並通過mvpDispatcher進行繫結
createPresenters()?.forEach { mvpDispatcher.addPresenter(it) }
}
}
複製程式碼
比如在最上面我們所舉例的LoginActivity。假設現在我們需要對登入頁再新增一個驗證碼校驗邏輯
. 此邏輯被放在了CaptchaPresenter
中:
class LoginActivity:BaseMVPActivity(),LoginView, CaptchaView {
// 登入的Presenter實現
val loginPresenter = LoginPresenter(this)
// 驗證碼的Presenter實現
val captchaPresenter = CaptchaPresenter(this)
// 繫結多個Presenter
override fun createPresenters() = arrayOf(loginPresenter, captchaPresenter)
...
}
複製程式碼
這就做到了Presenter的複用。在需要共用一些基礎業務邏輯
的時候。此一對多的繫結
是個很好的特性!
4. 藉助MVPDispatcher實現V-P生命週期關聯管理
abstract class BaseMVPActivity:Activity(), MVPView {
...// other codes
override fun onCreate(savedInstanceState: Bundle?) {
...
mvpDispatcher.dispatchOnCreate(intent?.extras)
}
...
override fun onDestroy() {
...
// 銷燬時mvpDispatcher會自動進行所有的Presenter的解綁。
// 所以具體的V層並不需要再自己去手動進行解綁操作了
mvpDispatcher.dispatchOnDestroy()
}
}
複製程式碼
這就是一個基本的V層基類實現類需要做到的事。到這裡。整個MVP基礎框架的搭建就算完成了!
5. MVP架構開源
由於此MVP架構其實是個挺簡單的架構。所以我將此架構原始碼存放在了EasyAndroid元件庫中了。
EasyAndroid作為一款整合元件庫,此庫中所整合的元件,均包含以下特點,你可以放心使用~~
1. 設計獨立
元件間獨立存在,不相互依賴,且若只需要整合庫中的部分元件。也可以很方便的
只copy對應的元件檔案
進行使用
2. 設計輕巧
因為是元件整合庫,所以要求每個元件的設計儘量精練、輕巧。避免因為一個小功能而引入大量無用程式碼.
每個元件的方法數均
不超過100
. 大部分元件甚至不超過50
。
由於V層基類實現
不同專案都會有一定的差異性。比如Activity基類選擇
(AppCompatActivity/v4Activity/Activity)、或者MVPView的展示樣式設計
等。所以BaseMVPActivity
這類真正的V層基類實現
並沒有被放入lib中。而是在示例工程中
單獨進行了提供.
在需要使用的時候,通過copy此部分的原始碼直接到工程中使用即可
EasyAndroid庫中的mvp模組。僅僅提供了MVPView
、MVPPresenter
、MVPDispatcher
這三個基礎支援類。避免引入無用程式碼。
原始碼連結
- EasyAndroid開源元件庫地址
https://github.com/yjfnypeu/EasyAndroid
- 基礎支援類
MVPView
、MVPPresenter
、MVPDispatcher
原始碼地址
https://github.com/yjfnypeu/EasyAndroid/tree/master/utils/src/main/java/com/haoge/easyandroid/mvp
V層基類實現
及簡單sample示例程式碼
地址
https://github.com/yjfnypeu/EasyAndroid/tree/master/app/src/main/java/com/haoge/sample/easyandroid/activities/mvp