MVP開(kāi)發(fā)模式漫談

前言

最近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 控制層,一般用ActivityFragment

隨著業(yè)務(wù)擴(kuò)展祷肯,版本迭代沉填,往往會(huì)發(fā)現(xiàn)一個(gè)上千行ActivityFragment,臃腫不堪看起來(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ā)布DatabindingMVVM成了更加新潮的選擇嚷量,利用數(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ù)涯鲁,通知PP則根據(jù)具體的業(yè)務(wù)需求有序,執(zhí)行VUI調(diào)度接口抹腿。

image.png

MVP的優(yōu)勢(shì)與問(wèn)題

利用MVP,原來(lái)MVCCActivityFragment膨脹的問(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()
                }

            })
        }
    }
}

眾所周知,MVCC層會(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ù)是字符串類型的usernamepassword,以及一個(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)系就變成了下圖所示:

image.png

至此屿讽,MVP三層之間再也沒(méi)有直接的關(guān)聯(lián)關(guān)系昭灵,他們想要調(diào)用下層,或者通知上層伐谈,都必須通過(guò)接口烂完。分層完成。

接口/基類抽取

這里討論的基類诵棵,主要指的是Activity或者Fragment抠蚣,至于自定義View,雖然也可以作為View層 的一個(gè)實(shí)例履澳,但是由于它的聲明周期是跟隨它所在的FragmentActivity嘶窄,所以這里不予討論浦箱。另外张弛,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

然后是基類抽取 BaseActivityBaseFragment

/**
 * 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)該是:

image.png

解決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ù)為字符串類型的usernamepassword聪轿。

  • View層爷肝,則需要校驗(yàn)界面用戶名,密碼參數(shù)是否為空陆错,我提供了一個(gè)checkParams方法灯抛,并且它需要處理登錄之后的回調(diào),我定一個(gè)接口 handleLoginResult音瓷。

  • Presenter層对嚼,它是要被View層調(diào)用的,我提供一個(gè)函數(shù) doLogin, 參數(shù)usernamepassword外莲。

此外猪半,提供2個(gè)共生體方法,getPresenter() 和 getModel() 只是為了讓開(kāi)發(fā)者統(tǒng)一的一個(gè)地方來(lái)獲取M和P偷线。至于V實(shí)例,一般都是ActivityFragment沽甥,這兩個(gè)東西声邦,ActivityAMS創(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è)方法 doLogindoLogin2,但是很明顯锦秒,如果把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ò)誤提示蚜厉。

最后的效果:

GIF.gif

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)心的只是onCreateonDestrory,這兩個(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類鬼譬,必須持有modelview的實(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)看下圖:

image-20200516170453865.png

框架代碼分離

基礎(chǔ)框架阳藻,和具體的app module的代碼晰奖,畢竟分屬不同的層級(jí)。使用時(shí)最好是分離到不同的module中腥泥。

image-20200516165859307.png

比較大團(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)指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載蹦疑,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者西雀。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市歉摧,隨后出現(xiàn)的幾起案子艇肴,更是在濱河造成了極大的恐慌腔呜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件再悼,死亡現(xiàn)場(chǎng)離奇詭異核畴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)冲九,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)谤草,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人莺奸,你說(shuō)我怎么就攤上這事丑孩。” “怎么了灭贷?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵温学,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我氧腰,道長(zhǎng)枫浙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任古拴,我火速辦了婚禮箩帚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黄痪。我一直安慰自己紧帕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布桅打。 她就那樣靜靜地躺著是嗜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挺尾。 梳的紋絲不亂的頭發(fā)上鹅搪,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音遭铺,去河邊找鬼丽柿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛魂挂,可吹牛的內(nèi)容都是我干的甫题。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼涂召,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坠非!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起果正,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炎码,失蹤者是張志新(化名)和其女友劉穎盟迟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體辅肾,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡队萤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了矫钓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡舍杜,死狀恐怖新娜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情既绩,我是刑警寧澤概龄,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站饲握,受9級(jí)特大地震影響私杜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜救欧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一衰粹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笆怠,春花似錦铝耻、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至办成,卻和暖如春泡态,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背迂卢。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工某弦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冷守。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓刀崖,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拍摇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子亮钦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355