前言
最近Review公司代碼,發(fā)現(xiàn)很多人只知MVP模式其名拼窥,而不知其原理戏蔑,濫用Presenter類,有些甚至都沒(méi)有用P接口鲁纠,導(dǎo)致項(xiàng)目難以維護(hù)总棵。翻閱了網(wǎng)絡(luò)上很多關(guān)于MVP的文章,大多數(shù)都沒(méi)有給出可研究的實(shí)例改含,筆者覺(jué)得情龄,寫(xiě)一個(gè)"相對(duì)標(biāo)準(zhǔn)"版的教程是有必要的。本文將從MVP開(kāi)發(fā)模式的思想,到對(duì)MVC遠(yuǎn)古代碼進(jìn)行改造骤视,一步一步提煉出MVP開(kāi)發(fā)框架鞍爱。并提供可研究的Github項(xiàng)目Demo。
正文大綱
- 最早的MVC
- 后來(lái)的MVP
- 新出爐的MVVM
- MVP模型圖
- MVP的優(yōu)勢(shì)和問(wèn)題
- MVP框架Demo解讀
- 完成核心功能
- 接口/基類抽取
- 解決Bug
- 框架代碼分離
正文
最早的MVC
最早的開(kāi)發(fā)模式是MVC专酗,
層級(jí) | 含義 |
---|---|
M | 數(shù)據(jù)層,純粹獲取數(shù)據(jù) |
V | 視圖層睹逃,一般指的是xml布局文件 |
C | 控制層,一般用Activity或Fragment |
隨著業(yè)務(wù)擴(kuò)展祷肯,版本迭代沉填,往往會(huì)發(fā)現(xiàn)一個(gè)上千行的Activity或Fragment,臃腫不堪看起來(lái)簡(jiǎn)直難受,而且難以維護(hù)佑笋。
后來(lái)的MVP
層級(jí) | 含義 |
---|---|
M | 依然是數(shù)據(jù)層翼闹,只負(fù)責(zé)數(shù)據(jù)的獲取.其他一改不管,傳入入?yún)⒑突卣{(diào)函數(shù)蒋纬,M層的代碼會(huì)調(diào)用回掉函數(shù)通知上一層 |
V | 視圖層猎荠,由之前的xml布局文件,擴(kuò)充到Activity/Fragment/自定義View/自定義ViewGroup 只要是UI界面相關(guān)的東西蜀备,全都?xì)w類到V層关摇。 |
P | 新概念,單詞 Presenter琼掠,翻譯為:表現(xiàn)層拒垃。功能類似于 粘合劑,它作為中間層瓷蛙,連接數(shù)據(jù)層M和視圖層V悼瓮, 專門(mén)處理 M和V這兩口子之間雜七雜八的破事兒。 |
M層艰猬,很純粹横堡,前面說(shuō)過(guò),只負(fù)責(zé)數(shù)據(jù)獲取以及通知上層冠桃。理論上來(lái)說(shuō)命贴,M層可以單獨(dú)進(jìn)行測(cè)試,檢查數(shù)據(jù)接口是否正常食听。V層胸蛛,也很純粹,V只負(fù)責(zé)界面元素的調(diào)度樱报,它不會(huì)去管任何和具體業(yè)務(wù)相關(guān)的事葬项。V層也可以安排假數(shù)據(jù)單獨(dú)進(jìn)行交互性測(cè)試。P, 則像個(gè)媒人迹蛤,把兩個(gè)純粹的"男女"撮合起來(lái)民珍。
最新出爐的MVVM
MVP優(yōu)于MVC襟士,但是隨著谷歌發(fā)布Databinding,MVVM成了更加新潮的選擇嚷量,利用數(shù)據(jù)模型的雙向綁定陋桂,開(kāi)發(fā)中確實(shí)可以節(jié)約不少代碼量,但是MVVM也有不盡如人意的地方蝶溶,比如Debug困難, 比如代碼侵入XML嗜历,比如學(xué)習(xí)成本較高等等。本文僅提及一下身坐,不作深入討論秸脱。
MVP模型圖
Activity作為標(biāo)準(zhǔn)的V落包,它調(diào)用P層的方法來(lái)執(zhí)行業(yè)務(wù)邏輯部蛇,P層則調(diào)用M層的接口來(lái)執(zhí)行數(shù)據(jù)獲取。
之后咐蝇,M層通過(guò)P給的callback回調(diào)函數(shù)涯鲁,通知P,P則根據(jù)具體的業(yè)務(wù)需求有序,執(zhí)行V的UI調(diào)度接口抹腿。
MVP的優(yōu)勢(shì)與問(wèn)題
利用MVP,原來(lái)MVC中C層 Activity或Fragment膨脹的問(wèn)題解決了旭寿。
但是隨之而來(lái)的警绩,由于我們用了P層來(lái)處理業(yè)務(wù)邏輯,隨著版本迭代盅称,業(yè)務(wù)量越來(lái)越多肩祥,舊代碼不敢刪,新代碼越來(lái)越多缩膝,P的代碼也逐漸膨脹混狠,于是又出現(xiàn)了和MVC相似的窘境。
沒(méi)事疾层,也有辦法将饺,使用接口約束,將大的P類痛黎,分成若干個(gè)小的P類予弧,保證代碼整潔清晰。一段時(shí)間之內(nèi)湖饱,可能確實(shí)有效果掖蛤。但是時(shí)間長(zhǎng)了,會(huì)發(fā)現(xiàn)P接口很多琉历,P實(shí)例類也很多坠七,可能還存在各種錯(cuò)綜復(fù)雜的繼承關(guān)系水醋,難以管理,找一個(gè)業(yè)務(wù)的Bug彪置,看到一大堆的P類拄踪,腦殼也是很疼的。
于是 Contract 思想來(lái)幫我們解決這個(gè)問(wèn)題拳魁。
所謂Contract惶桐,翻譯為:合同,契約潘懊。它負(fù)責(zé)來(lái)對(duì)某一個(gè)業(yè)務(wù)的M V P三層進(jìn)行統(tǒng)籌管理姚糊,如果你要去找一個(gè) 業(yè)務(wù)Bug,很簡(jiǎn)單授舟,找到該業(yè)務(wù)的Contract類救恨,所有的M層,V層,P層接口一目了然释树。
class XXXContract{
interface Model{xxx}
interface View{xxx}
interface Presenter{xxx}
}
但是肠槽,問(wèn)題總是源源不斷。后續(xù)的奢啥,還有MVP中的內(nèi)存泄漏的問(wèn)題秸仙,因?yàn)?strong>P層要持有V層的引用。以及復(fù)雜業(yè)務(wù)下Contract的管理問(wèn)題,需要一定的學(xué)習(xí)研究時(shí)間成本桩盲。
而且最關(guān)鍵的是寂纪,我們做一個(gè)MVP開(kāi)發(fā)框架,是為了什么赌结?是為了立標(biāo)準(zhǔn)捞蛋。立標(biāo)準(zhǔn)是為了團(tuán)隊(duì)協(xié)作開(kāi)發(fā)的可持續(xù)發(fā)展,保證項(xiàng)目代碼可以健康地發(fā)展迭代,而不用為了重構(gòu)而大費(fèi)精力姑曙。但是襟交,思考一下,一個(gè)很小的業(yè)務(wù)伤靠,一定需要我們定MVP三層來(lái)實(shí)現(xiàn)么捣域?也不一定。
MVP框架Demo解讀
開(kāi)發(fā)一個(gè)MVP框架宴合,除了框架源碼之外焕梅,還需要一個(gè)完整的文檔資料,供使用者閱讀學(xué)習(xí)卦洽。
本文Demo地址:https://github.com/18598925736/MvpDemo贞言,建議下載源碼對(duì)照閱讀。
一般框架的開(kāi)發(fā)設(shè)計(jì)思路都是阀蒂,
-
完成核心功能
MVP開(kāi)發(fā)框架的核心功能该窗,就是分離 M模型層弟蚀,V視圖層 和 P控制層
-
接口/基類抽取
對(duì)于常用的類,定義統(tǒng)一的抽象類或者接口酗失,來(lái)約束該類的行為义钉。這里必須對(duì)Activity和Fragment,還有自定義View進(jìn)行抽取规肴,因?yàn)樗麄兌加锌赡艹蔀?V視圖層捶闸。 這里可能會(huì)用到 泛型約束。
-
解決Bug
為了快速實(shí)現(xiàn)功能拖刃,可能會(huì)存在一些隱藏Bug删壮,比如MVP的內(nèi)存泄漏問(wèn)題。解決MVP內(nèi)存泄漏帶來(lái)的View空指針問(wèn)題等等兑牡。
-
框架代碼分離
框架代碼應(yīng)該單獨(dú)存在一個(gè)module中央碟,以便團(tuán)隊(duì)復(fù)用。
接下來(lái)以這4個(gè)步驟分別解讀, 以一個(gè)最簡(jiǎn)單的登錄功能為例:
完成核心功能
要分層发绢,首先要找好分層的對(duì)象硬耍。先來(lái)看一段遠(yuǎn)古MVC代碼,標(biāo)準(zhǔn)的C層寫(xiě)法. 其中M層,我使用的是HttpRequestManager边酒,統(tǒng)一管理所有網(wǎng)絡(luò)請(qǐng)求,V層X(jué)ML我就不展示了:
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
btnLogin.setOnClickListener {
val username = tvUsername.text.trim().toString()
val password = tvPassword.text.trim().toString()
progressBar.visibility = View.VISIBLE
HttpRequestManager().doLogin(username, password, object : HttpCallback<UserBean> {
override fun onSuccess(result: UserBean?) {
progressBar.visibility = View.INVISIBLE
dataView.text = result.toString()
}
override fun onFailure(e: Exception?) {
progressBar.visibility = View.INVISIBLE
dataView.text = e.toString()
}
})
}
}
}
眾所周知,MVC的C層會(huì)隨著業(yè)務(wù)無(wú)限膨脹狸窘。這里我要將其中的業(yè)務(wù)邏輯抽離出來(lái)形成P層墩朦。再把 HttpRequestManager 封裝到M層。V層 原本是XML》埽現(xiàn)在把Activity擴(kuò)充為V層的一部分氓涣。
分層架構(gòu)有一個(gè)鐵則:接口分離。分層之后陋气,層級(jí)之間劳吠,不允許存在類和類的直接依賴關(guān)系, 必須通過(guò)接口進(jìn)行約束,要建立依賴巩趁,也必須通過(guò)接口痒玩。具體做法如下:
先分析M層,網(wǎng)絡(luò)請(qǐng)求是doLogin议慰,傳入?yún)?shù)是字符串類型的username和password,以及一個(gè)HttpCallback回調(diào)蠢古。抽離成 LoginModel , 以及 它的實(shí)現(xiàn)類 LoginModelImpl:
interface LoginModel {
fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
}
class LoginModelImpl : LoginModel {
override fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>) {
HttpRequestManager().doLogin(username, password, httpCallback)
}
}
然后是,V層别凹,如果僅僅考慮UI元素調(diào)度的話草讶,V層中必須存在這么幾個(gè)方法,用接口來(lái)約束:
interface LoginView {
fun showLoading() // 展示加載中
fun hideLoading() // 隱藏加載中
fun onError(msg: String) // 展示報(bào)錯(cuò)信息
fun onSuccess(msg: String) // 展示成功信息
}
將Activity抽離成V層:
class LoginActivity : AppCompatActivity(), LoginView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
btnLogin.setOnClickListener {}
}
override fun showLoading() {
progressBar.visibility = View.VISIBLE
}
override fun hideLoading() {
progressBar.visibility = View.INVISIBLE
}
override fun onError(msg: String) {
dataView.text = msg
}
override fun onSuccess(msg: String) {
dataView.text = msg
}
}
然后就是重點(diǎn) P層,它是 M和V層的連接點(diǎn)炉菲,它負(fù)責(zé)調(diào)度M層的業(yè)務(wù)接口堕战,也負(fù)責(zé)通知V層更新UI坤溃。它目前的核心任務(wù)就是,doLogin嘱丢,用接口來(lái)表示:
interface LoginPresenter {
fun doLogin(username: String, password: String)
}
三層已經(jīng)分離完畢浇雹。接下來(lái)建立接口依賴。
先用P層連接V和M.
class LoginPresenterImpl : LoginPresenter {
var model: LoginModel?
var view: LoginView?
constructor(view: LoginView) {
this.view = view
model = LoginModelImpl()
}
override fun doLogin(username: String, password: String) {
val m = model ?: return
val v = view ?: return
v.showLoading()
m.doLogin(username, password, object : HttpCallback<UserBean> {
override fun onSuccess(result: UserBean?) {
v.hideLoading()
v.onSuccess(result.toString())
}
override fun onFailure(e: Exception?) {
v.hideLoading()
v.onError(e.toString())
}
})
}
}
然后用V連接P:
class LoginActivity : AppCompatActivity(), LoginView {
private val presenter: LoginPresenter = LoginPresenterImpl(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
btnLogin.setOnClickListener {
val username = tvUsername.text.trim().toString()
val password = tvPassword.text.trim().toString()
presenter.doLogin(username, password)
}
}
...
}
到目前為止:MVP三層之間的關(guān)系就變成了下圖所示:
至此屿讽,MVP三層之間再也沒(méi)有直接的關(guān)聯(lián)關(guān)系昭灵,他們想要調(diào)用下層,或者通知上層伐谈,都必須通過(guò)接口烂完。分層完成。
接口/基類抽取
這里討論的基類诵棵,主要指的是Activity或者Fragment抠蚣,至于自定義View,雖然也可以作為View層 的一個(gè)實(shí)例履澳,但是由于它的聲明周期是跟隨它所在的Fragment和Activity嘶窄,所以這里不予討論浦箱。另外张弛,M和P層也定義接口來(lái)約束行為难衰,哪怕是空接口哆致,也是有必要的糕珊,避免業(yè)務(wù)擴(kuò)展時(shí)猝不及防逛绵。
先看接口抽取:
三個(gè)頂層接口
/**
* 模型基準(zhǔn)接口,
* 目前是暫時(shí)空白
*/
interface BaseModel {
}
interface BasePresenter<V : BaseView> {
}
/**
* 規(guī)定所有V層 對(duì)象共有的行為
*/
interface BaseView {
/**
* 顯示加載中
*/
fun showLoading()
/**
* 隱藏加載
*/
fun hideLoading()
/**
* 當(dāng)數(shù)據(jù)加載失敗時(shí)
*/
fun onError(msg: String)
}
M和P的接口暫時(shí)是空奋构,V的接口 BaseView約束一些V層實(shí)例所共有的特性乌企,當(dāng)數(shù)據(jù)加載時(shí)阁最,可能需要顯示進(jìn)度條菊花戒祠,當(dāng)數(shù)據(jù)加載失敗時(shí),可能要做出提示速种。那數(shù)據(jù)加載成功時(shí)為什么沒(méi)有定方法呢姜盈? 這是因?yàn)椋瑪?shù)據(jù)加載成功之后配阵,界面所要加載的JavaBean不盡相同馏颂,我嘗試過(guò)很多次使用泛型來(lái)進(jìn)行約束,效果都不理想闸餐,最后把 onSuccess放到了更下層接口中饱亮。 而,interface BasePresenter<V : BaseView>
增加泛型參數(shù)舍沙,是為了約束實(shí)現(xiàn)類所綁定的View層實(shí)例近上,要求View層實(shí)例必須實(shí)現(xiàn)BaseView。
然后是基類抽取 BaseActivity 和 BaseFragment:
/**
* Activity 基 類
* 使用該類創(chuàng)建實(shí)體Activity類拂铡,必須在泛型中先指定它的 P類
*/
abstract class BaseActivity<T : BasePresenter<BaseView>> : AppCompatActivity() {
/**
* 布局ID
*/
abstract fun getLayoutId(): Int
/**
* 界面元素初始化
*/
abstract fun init()
/**
* 業(yè)務(wù)處理類P
*/
lateinit var mPresenter: BasePresenter<BaseView>
/**
* P類對(duì)象強(qiáng)轉(zhuǎn), 強(qiáng)轉(zhuǎn)之后才可以在V層使用
*/
abstract fun castPresenter(): T
/**
* 綁定業(yè)務(wù)處理類對(duì)象
*/
abstract fun bindPresenter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(getLayoutId())
bindPresenter()
init()
}
}
abstract class BaseFragment<T : BasePresenter<BaseView>> : Fragment() {
/**
* 布局ID
*/
abstract fun getLayoutId(): Int
/**
* 界面元素初始化
*/
abstract fun init(view: View)
/**
* 業(yè)務(wù)處理類P
*/
lateinit var mPresenter: BasePresenter<BaseView>
/**
* P類對(duì)象強(qiáng)轉(zhuǎn), 強(qiáng)轉(zhuǎn)之后才可以在V層使用
*/
abstract fun castPresenter(): T
/**
* 綁定業(yè)務(wù)處理類對(duì)象
*/
abstract fun bindPresenter()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(getLayoutId(), container, false)
bindPresenter()
init(root)
return root
}
}
abstract class BaseFragment<T : BasePresenter<BaseView>>
abstract class BaseActivity<T : BasePresenter<BaseView>>
增加泛型約束是為了在保持三層架構(gòu)接口隔離的同時(shí)壹无,能夠調(diào)用到 真正的P實(shí)例的方法葱绒。
那么,既然 MVP頂層接口和Activity基類有了斗锭,上面的 LoginView,LoginActivty,LoginModel地淀,LoginPresenter, 代碼就要變更成這樣:
interface LoginView : BaseView {// 繼承BaseView的行為
fun onSuccess(msg: String)// 并且有自己特有的行為
}
interface LoginModel : BaseModel {// 繼承BaseModel
fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
}
interface LoginPresenter : BasePresenter<BaseView> {// 繼承BasePresenter
fun doLogin(username: String, password: String)
}
class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {// 實(shí)現(xiàn)LoginView的所有行為
// 泛型設(shè)為 LoginPresenter
override fun showLoading() {
progressBar.visibility = View.VISIBLE
}
override fun hideLoading() {
progressBar.visibility = View.INVISIBLE
}
override fun onError(msg: String) {
dataView.text = msg
}
override fun onSuccess(msg: String) {
dataView.text = msg
}
override fun getLayoutId(): Int {
return R.layout.activity_login
}
override fun init() {
btnLogin.setOnClickListener {
val username = tvUsername.text.trim().toString()
val password = tvPassword.text.trim().toString()
castPresenter().doLogin(username, password)
}
}
override fun castPresenter(): LoginPresenter {
return mPresenter as LoginPresenterImpl
}
override fun bindPresenter() {
mPresenter = LoginPresenterImpl(this)
}
}
接口和基類抽取都完成了。那么稍加整理之后岖是,項(xiàng)目結(jié)構(gòu)應(yīng)該是:
解決Bug
那么來(lái)解決上文提到的霸哥吧
P層膨脹
接口過(guò)多帮毁,P實(shí)現(xiàn)類也過(guò)多,Contract 思維.
上代碼:
class LoginContract {
/**
* 定義數(shù)據(jù)接口
*/
interface Model : BaseModel {
fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>)
}
/**
* 定義View層的界面處理
*/
interface View : BaseView {
fun checkParams(): Boolean
fun handleLoginResult(result: UserBean?)
}
/**
* 定義P層的業(yè)務(wù)邏輯調(diào)用
*/
interface Presenter : BasePresenter<BaseView> {
fun doLogin(username: String, password: String)
}
// 這里是不是可以提供靜態(tài)方法,得到具體的P和M對(duì)象
companion object {
fun getPresenter(view: View): Presenter {
return LoginPresenter(view)
}
fun getModel(): Model {
return LoginModel()
}
}
}
一個(gè)登錄業(yè)務(wù)豺撑,
Model層烈疚,只需要提供一個(gè)doLogin方法即可,參數(shù)為字符串類型的username和password聪轿。
View層爷肝,則需要校驗(yàn)界面用戶名,密碼參數(shù)是否為空陆错,我提供了一個(gè)checkParams方法灯抛,并且它需要處理登錄之后的回調(diào),我定一個(gè)接口 handleLoginResult音瓷。
Presenter層对嚼,它是要被View層調(diào)用的,我提供一個(gè)函數(shù) doLogin, 參數(shù)username和password外莲。
此外猪半,提供2個(gè)共生體方法,getPresenter() 和 getModel() 只是為了讓開(kāi)發(fā)者統(tǒng)一的一個(gè)地方來(lái)獲取M和P偷线。至于V實(shí)例,一般都是Activity和Fragment沽甥,這兩個(gè)東西声邦,Activity是AMS創(chuàng)建實(shí)例的,不用我們多管,想管也管不著摆舟,Fragment 則一般會(huì)手動(dòng)去new 或者 用Fragment的靜態(tài)方法 getInstance亥曹。所以此處不提供get方法。這里并不是一定要用共生體靜態(tài)方法恨诱,也可以把Contract類寫(xiě)成單例媳瞪,這里我節(jié)約時(shí)間沒(méi)有這么做。
有了 LoginContract 類之后照宝,再去 按照這里面約束的M,V,P接口去創(chuàng)建MVP三層的實(shí)體類蛇受。比如上面的M:
open class LoginModel : LoginContract.Model {
override fun doLogin(username: String, password: String, httpCallback: HttpCallback<UserBean>) {
HttpRequestManager.doLogin(username, password, httpCallback)
}
}
open class LoginActivity : BaseActivity<LoginContract.Presenter>(), LoginContract.View {
...
override fun init() {
btnLogin.setOnClickListener {
if (checkParams()) {
val username = tvUsername.text.trim().toString()
val password = tvPassword.text.trim().toString()
castPresenter().doLogin(username, password)
} else {
onError("有參數(shù)為空..")
}
}
}
override fun checkParams(): Boolean {
return tvUsername.text.isNotEmpty() && tvPassword.text.isNotEmpty()
}
override fun handleLoginResult(result: UserBean?) {
Log.d("handleLoginResult", result.toString())
dataView.text = result.toString()
}
...
}
open class LoginPresenter(view: LoginContract.View) : LoginContract.Presenter {
//P類,持有M和V的引用
// 為什么我要把 model 放在外面厕鹃?一個(gè)業(yè)務(wù)類P兢仰,只會(huì)有一個(gè)model么乍丈?如果需要多個(gè)數(shù)據(jù)源呢?
var model: LoginContract.Model? = null
var view: LoginContract.View? = view
override fun doLogin(username: String, password: String) {
val v = view ?: return
val m = model ?: return
v.showLoading()
m.doLogin(username, password, object : HttpCallback<UserBean> {
override fun onSuccess(result: UserBean?) {
v.hideLoading()
v.handleLoginResult(result)
}
override fun onFailure(e: Exception?) {
v.hideLoading()
v.onError(e.toString())
}
})
}
...
}
這樣就能把一個(gè)業(yè)務(wù)的MVP三層統(tǒng)籌管理把将。韓信點(diǎn)兵多多益善轻专,兵多不是問(wèn)題,只要有擅長(zhǎng)統(tǒng)兵的將領(lǐng)察蹲,我把將領(lǐng)管理好就行请垛。
這里也是一樣,再多的Presenter類洽议,Model類宗收,業(yè)務(wù)再?gòu)?fù)雜, 只要管理有方绞铃,就不會(huì)天下大亂镜雨。Contract層正是我們的統(tǒng)兵將領(lǐng)
MVP復(fù)用問(wèn)題
有了Contract,問(wèn)題就真的完全解決了么?非也儿捧!
隨著產(chǎn)品錦鯉的腦洞大開(kāi)荚坞,各種奇葩的業(yè)務(wù)又來(lái)了。
比如:
之前有一個(gè)登錄業(yè)務(wù)菲盾,一切都好好的颓影,突然,產(chǎn)品要求懒鉴,在原來(lái)的基礎(chǔ)上诡挂,增加一個(gè)SSSVIP登錄的功能,原來(lái)的登錄業(yè)務(wù)代碼保留临谱,因?yàn)?strong>普通用戶還用得著璃俗,SSSVIP爸爸客戶們要用尊貴的 登錄界面,What the ***!
之前我們用的MVP開(kāi)發(fā)模式悉默,加入了Contract層 統(tǒng)籌管理所有的MVP三層的所有類城豁。試想一下,是不是每一個(gè)業(yè)務(wù)都需要開(kāi)辟自己的MVP三層抄课?答案是否定的唱星,比如這里的 SSSVIP登錄業(yè)務(wù), 99%的業(yè)務(wù)代碼可能都是一摸一樣的,唯一不同的就是 登錄接口要新增傳入一個(gè)新的UserType=SSSVIP參數(shù)而已跟磨,那之前的 登錄業(yè)務(wù)代碼還用得著么?
當(dāng)然用得著,作為一個(gè)有潔癖的程序猿间聊,我們不允許重復(fù)代碼。請(qǐng)看:
但是要說(shuō)一句抵拘,每一個(gè)獨(dú)立的業(yè)務(wù)都有自己的Contract哎榴,這是一定的,因?yàn)?strong>Contract就代表了當(dāng)前業(yè)務(wù)的統(tǒng)兵將領(lǐng)。SSSVIP登錄業(yè)務(wù)的Contract如下:
class LoginContract2 {
interface Model : LoginContract.Model {
fun doLogin2(
username: String,
password: String,
userType: String,
httpCallback: HttpCallback<UserBean>
)
}
interface View : LoginContract.View {
fun getUserType(): String
fun handleLoginResultForSSSVIP(result: UserBean?)
fun onErrorForSSSVIP(msg: String)
}
interface Presenter : LoginContract.Presenter {
fun doLogin2(username: String, password: String, userType: String)
}
// 這里是不是可以提供靜態(tài)方法叹话,得到具體的P和M對(duì)象
companion object {
fun getPresenter(view: View): Presenter {
return LoginPresenter2(view)
}
fun getModel(): Model {
return LoginModel2()
}
}
}
上面的代碼中偷遗,
內(nèi)部接口 Model繼承自 之前LoginContract.Model,這意味著,之前的登錄接口可以復(fù)用驼壶。
內(nèi)部接口View繼承自 之前LoginContract.View氏豌,之前登錄View層的約束不用重復(fù)寫(xiě)一遍了。
內(nèi)部接口Presenter也繼承自 之前LoginContract.Presenter热凹。
繼承之后泵喘,只需要增加SSSVIP登錄所需的特別方法就可以了,前面的邏輯完全復(fù)用起來(lái)了般妙。
Model新增的接口:doLogin2() 只是新增了一個(gè)userType參數(shù)
View層新增了3個(gè)接口纪铺。
- getUserType()獲取當(dāng)前的userType,
- handleLoginResultForSSSVIP()尊貴的VIP怎么可以和普通用戶用一樣的登錄回調(diào)呢碟渺?安排起來(lái)鲜锚。
- 最后的 onErrorForSSSVIP()接口,讓SSSVIP的登錄報(bào)錯(cuò)也與眾不同苫拍。
P層芜繁,新增一個(gè)doLogin2()接口,和原來(lái)相比多了一個(gè)userType參數(shù)绒极,V層調(diào)用這個(gè)接口把userType傳遞給P骏令,最終給到M。
剩下的共生體垄提,沒(méi)有變化榔袋,只是為了讓開(kāi)發(fā)者在統(tǒng)一的地方獲得M和P的實(shí)例。
Contract的統(tǒng)籌之下铡俐,MVP三層復(fù)用問(wèn)題解決了凰兑。那么接下來(lái)就是SSSVIP登錄業(yè)務(wù)的 MVP三層實(shí)例,如何防止重復(fù)代碼审丘。
先看:Model
class LoginModel2 : LoginContract2.Model, LoginModel() {
override fun doLogin2(
username: String,
password: String,
userType: String,
httpCallback: HttpCallback<UserBean>
) {
HttpRequestManager.doLogin2(username, password, userType, httpCallback)
}
}
它要繼承 LoginContract2.Model接口 聪黎,就必須實(shí)現(xiàn) 該接口的方法,但是由于 LoginContract2.Model 接口繼承了 LoginContract.Model 备恤,原則上在這里必須實(shí)現(xiàn)這兩個(gè)方法 doLogin和doLogin2,但是很明顯锦秒,如果把doLogin再寫(xiě)一遍露泊,那就太low了。解決方式為:在實(shí)現(xiàn) LoginContract2.Model 的同時(shí)旅择,繼承LoginModel類惭笑。這樣,即可以實(shí)現(xiàn)SSSVIP特有的M層接口,又不用把普通用戶的登錄Model方法再寫(xiě)一遍沉噩。
剩下的 V和P也是類似:
class LoginPresenter2(view: LoginContract2.View) : LoginPresenter(view), LoginContract2.Presenter {
override fun doLogin2(username: String, password: String, userType: String) {
val m = model as LoginContract2.Model // 類型轉(zhuǎn)換成 Login2Activity專用的 Model
val v = view as LoginContract2.View
v.showLoading()
m.doLogin2(username, password, userType, object : HttpCallback<UserBean> {
override fun onSuccess(result: UserBean?) {
v.hideLoading()
v.handleLoginResultForSSSVIP(result)
}
override fun onFailure(e: Exception?) {
v.hideLoading()
v.onErrorForSSSVIP(e.toString())
}
})
}
...
}
doLogin不用再寫(xiě)一遍捺宗。
class LoginActivity2 : LoginActivity(), LoginContract2.View {
override fun getLayoutId(): Int {
return R.layout.activity_login2
}
override fun init() {
super.init()
// SSSVIP登錄
btnLogin2.setOnClickListener {
if (checkParams()) {
val username = tvUsername.text.trim().toString()
val password = tvPassword.text.trim().toString()
castPresenter().doLogin2(username, password, getUserType())
} else {
onErrorForSSSVIP("有參數(shù)為空..")
}
}
}
override fun getUserType(): String {
return "SSSSVIP"
}
override fun handleLoginResultForSSSVIP(result: UserBean?) {
// 為SSSVIP專門(mén)準(zhǔn)備的登錄結(jié)果處理
Log.d("handleLoginResult", result.toString())
dataView.text = "尊貴的 ${getUserType()} \n${result.toString()}"
}
override fun onErrorForSSSVIP(msg: String) {
dataView.text = "尊貴的${getUserType()} \n$msg"
}
}
LoginActivity2中,實(shí)現(xiàn)了尊貴VIP專享的登錄結(jié)果回調(diào)川蒙,以及登錄錯(cuò)誤提示蚜厉。
最后的效果:
MVP三層,代碼重用的問(wèn)題也OK了畜眨。
內(nèi)存泄漏
到了這里MVP的所有問(wèn)題都解決了么昼牛?并沒(méi)有。作為一個(gè)MVP架構(gòu)康聂,P層需要持有V層的引用贰健,如果持有的是Activity,那么當(dāng)Activity自己finish了自己恬汁,如果發(fā)現(xiàn)有另外的對(duì)象持有了Activity的引用伶椿,并且這個(gè)對(duì)象還是GCRoot(比如它正在執(zhí)行耗時(shí)方法)那么Activity也是不能回收的。這種內(nèi)存泄漏的問(wèn)題有很多說(shuō)法氓侧,網(wǎng)上也有很多解決方案脊另,比較常見(jiàn)的就是,定義一個(gè)基類BasePresenter甘苍,提供一個(gè)抽象方法 abstract fun release()尝蠕,要求所有的子類都去調(diào)用它來(lái)釋放掉 View的引用,然后 在 BaseActivity中去調(diào)用這個(gè) release()方法 载庭。 這種做法確實(shí)可以 防止內(nèi)存泄漏看彼,但是隨著jetpack開(kāi)源組件的推廣普及,出現(xiàn)了更加簡(jiǎn)潔的寫(xiě)法囚聚,Lifecycle. 使用LifeCycle可以比傳統(tǒng)方法更加簡(jiǎn)潔優(yōu)雅地處理內(nèi)存泄漏靖榕。
定義一個(gè)基類 BasePresenter, 讓他實(shí)現(xiàn)LifecycleObserver接口, 然后所有的P類實(shí)例就都變成了生命周期的觀察者,可以隨時(shí)接收View層生命周期的變化顽铸。當(dāng)然茁计,目前我們關(guān)心的只是onCreate 和 onDestrory,這兩個(gè)生命周期關(guān)系著 view層的綁定和解綁谓松。
interface BasePresenter<V : BaseView> : LifecycleObserver {
/**
* 自動(dòng)感知Activity/Fragment 的 onCreate生命周期星压,開(kāi)始初始化一些全局變量
*
*
*/
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate()
/**
* 自動(dòng)感知Activity/Fragment 的 onDestroy生命周期,釋放全局變量
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy()
}
依然用LoginPresenter舉例:
open class LoginPresenter(view: LoginContract.View) : LoginContract.Presenter {
//P類,持有M和V的引用
var model: LoginContract.Model? = null
var view: LoginContract.View? = view
override fun doLogin(username: String, password: String) {
...
}
override fun onCreate() {
model = LoginContract.getModel()
}
override fun onDestroy() {
model = null
view = null
}
}
一個(gè)P類鬼譬,必須持有model和view的實(shí)例娜膘,model用來(lái)調(diào)用數(shù)據(jù)接口,view用來(lái)調(diào)用界面元素优质。上面的代碼中竣贪,view的綁定军洼,我用構(gòu)造函數(shù)來(lái)傳遞,時(shí)機(jī)上和onCreate相同演怎。而在view onDestroy的時(shí)候匕争,直接讓view=null釋放引用. 這樣,在Activity onDestroy爷耀,即將回收的時(shí)候甘桑,引用鏈斷開(kāi)了,杜絕內(nèi)存泄漏畏纲。Fragment的處理也是類似扇住。
當(dāng)然,還有一個(gè)重要步驟盗胀,注冊(cè)觀察者到View實(shí)例中
abstract class BaseActivity<T : BasePresenter<BaseView>> : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycle.addObserver(mPresenter) // 利用 lifecycle 防止內(nèi)存泄漏
}
}
LifeCycle使用非常簡(jiǎn)單艘蹋,原理上這里就不細(xì)講了,非本文重點(diǎn)票灰。
內(nèi)存泄漏解決之后的空指針問(wèn)題
比較容易忽略的是女阀,防止內(nèi)存泄漏之后的目的達(dá)成了,如果在P執(zhí)行M層數(shù)據(jù)的過(guò)程中屑迂,Activity被回收浸策,P類的view成員被置為NULL,就很有可能報(bào)出空指針異常惹盼,造成崩潰庸汗,所以,在P類中手报,用到view的地方蚯舱,最好都判空,因?yàn)椴灰欢?strong>view什么時(shí)候會(huì)解綁掩蛤⊥骰瑁或者整個(gè)方法拋出空指針異常,也可以揍鸟。比較簡(jiǎn)單兄裂,這個(gè)就不舉代碼實(shí)例了。請(qǐng)看下圖:
框架代碼分離
基礎(chǔ)框架阳藻,和具體的app module的代碼晰奖,畢竟分屬不同的層級(jí)。使用時(shí)最好是分離到不同的module中腥泥。
比較大團(tuán)隊(duì)畅涂,或者比較正規(guī)的團(tuán)隊(duì),都會(huì)把基礎(chǔ)框架打包成AAR給公司開(kāi)發(fā)組去引用道川,或者放置到本地倉(cāng)庫(kù)中,使用Gradle來(lái)進(jìn)行依賴,來(lái)達(dá)成框架共用的目的冒萄。分離之后還有一個(gè)好處臊岸,就是當(dāng)變更框架時(shí),所有使用到框架的開(kāi)發(fā)者都會(huì)感知框架的變化尊流,從而做出調(diào)整帅戒,讓整個(gè)團(tuán)隊(duì)的開(kāi)發(fā)節(jié)奏保持一致,有利于提高開(kāi)發(fā)效率崖技,減少?zèng)]必要的溝通成本逻住。
結(jié)語(yǔ)
框架有了,規(guī)矩也有有了迎献。如果人人都按照框架來(lái)開(kāi)發(fā)瞎访,那么項(xiàng)目的維護(hù)成本就會(huì)大大降低。那是不是每個(gè)業(yè)務(wù)都要按照框來(lái)做呢吁恍?不能這么絕對(duì)扒秸,如果實(shí)在是一個(gè)非常小的頁(yè)面,很小的功能冀瓦,使用MVP分層伴奥,反而顯得有點(diǎn)大材小用。所以翼闽,到底用還是不用拾徙,應(yīng)該視情況而定。但是有一點(diǎn)可以肯定感局,有了框架約束尼啡,項(xiàng)目重構(gòu)起來(lái)會(huì)更加的順暢,對(duì)團(tuán)隊(duì)只有好處沒(méi)有壞處蓝厌。
筆者水平有限玄叠,粗略提煉了這個(gè)MVP開(kāi)發(fā)框架,github地址為:https://github.com/18598925736/MvpDemo 拓提。如果讀者有心的話读恃,可以根據(jù)本文思路,對(duì)框架進(jìn)行完善代态,歡迎Fork 寺惫。發(fā)現(xiàn)有錯(cuò)誤的地方也歡迎批評(píng)指正。