Android架構設計:手把手教你擼一個簡潔而強大的MVP框架!

Haoge發表於2018-07-18

寫在前面

Android端的MVP架構已經出來有很長時間了。而對於Android的MVP實現模式,也並沒有個標準的實現方式。

現在市面上最流行的是google開源出來的一套MVP模型,此模型可到此google家MVP開源地址進行檢視。

而此篇部落格將要介紹的並不是google的MVP模型。而是根據我自身理解所建立的一種MVP模型。與google的MVP模型相比,此種MVP模型具有以下一些優勢:

  1. 支援單頁面繫結多個Presenter進行使用,便於進行Presenter複用
  2. 對Presenter進行生命週期派發,
  3. 自動對Presenter進行View的繫結與解綁。
  4. 剔除契約類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通訊流程,可分為以下幾步:

  1. V層向P層發起具體的處理任務
  2. P層接收到V層發起的任務。呼叫M層的api,獲取到原始資料
  3. P層對從M層獲取到的原始資料進行預處理(本示例因為較簡單,故沒有這一步)。
  4. 處理完畢後的資料。通過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?) {...}
}
複製程式碼

可以看到此聯結器幹了以下幾件事:

  1. addPresenterremovePresenter:向容器presenters中新增或移除presenter例項做到對單頁面繫結多個presenter的效果。
  2. dispatchOnXXX:通過已新增的presenter進行生命週期通知. 做到V-P生命週期繫結的效果
  3. 在接收到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模組。僅僅提供了MVPViewMVPPresenterMVPDispatcher這三個基礎支援類。避免引入無用程式碼。

原始碼連結

  • EasyAndroid開源元件庫地址

https://github.com/yjfnypeu/EasyAndroid

  • 基礎支援類MVPViewMVPPresenterMVPDispatcher原始碼地址

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

相關文章