談一談我對mvp框架的理解

我是最近才開始寫Android文章,暫時不知道該寫些什么東西。外加上一位朋友好像對mvp有點疑問。我本不想一開始就寫這個,但是我又不耐煩的去給他講什么mvp,mvp該怎么寫。我想了一下,與其一點一點告訴他什么是mvp,還不如寫下一篇文章來分享我關于MVP的一些理解。

說在前面

首先,在我的觀點里面,閱讀該源碼是需要有一點Android的開發經驗的。如果你只是一個初學者或者是沒有基礎的小伙子,我奉勸你別花費時間來閱讀我這篇文章,可能對你的發展并沒有多大的作用。

然后談到框架,其實首先映入眼簾的應該是mvc框架,這是最早在學習java的時候常見的。m是model層,v是view層、c是control層。這篇文章呢?我希望由mvc的概念講起、延伸至mvp的概念,然后再簡單的寫一個mvp的demo、到最后實際來封裝出一個在我掌控之內的mvp框架。結尾希望能結合我的一些開發經驗談一談mvp的優劣勢。

一、 mvc

首先mvc框架共分為三層。m是實體層用來組裝數據的;v是視圖層用來顯示數據的;c是控制層用來分發用戶的操作給視圖層。總的來說,基本的流程應該是下圖:

image.png

簡單的來說mvc的運行流程就是:用戶通過控制層去分發操作到實體層去組裝數據,最后將數據展示到視圖層的過程。

如果按照Android如今的分法的話,原本的實體層里面就應該還是實體層,然后fragment/activity里面就會富含生命周期、業務邏輯、視圖的操作等等。這樣做的好處呢?是代碼量比較統一,易于查找。

但是當業務邏輯比較復雜的時候呢?就會出現代碼量比較龐大,我甚至在之前的一個項目內看到了將近2000行的一個activity。當時我驚了個呆。由于剛接觸那個項目,我調試、log等等一系列操作都用上了,硬是用了三天才搞清楚代碼的流程。

作為一個有追求的程序員,也為了成為一個有責任心的程序員。我建議你看一看mvp。

二、 mvp

前面談到在mvc里面,業務邏輯層和視圖都會放在activity/fragment里面進行操作,并且本身activity就需要維護自己的生命周期。這會導致activity/fragment里面代碼的臃腫,減少代碼的可讀性和代碼的可維護性。

在我看來mvp框架其實是mvc框架變種產品。講原本的activity/fragment的層次劃分成present層和view層。m還是原來的實體層用來組裝數據,p層則用來隔離view層,被稱為中介層,v層還是view層主要用來展示數據的層。如下圖所示:

有了present層之后呢?view層就專心在activity/fragment里面主要去處理視圖層和維護自己的生命周期,將業務邏輯委托給present層,present層作為實體層和視圖層的中介。實體層和視圖層不直接進行交互,而是通過委托給persent層進行交互,這樣做的好處是:

  • 分離了視圖邏輯和業務邏輯,降低了耦合
  • Activity只處理生命周期的任務,代碼變得更加簡潔
  • 視圖邏輯和業務邏輯分別抽象到了View和Presenter的接口中去,提高代碼的可閱讀性
  • Presenter被抽象成接口,可以有多種具體的實現,所以方便進行單元測試
  • 把業務邏輯抽到Presenter中去,避免后臺線程引用著Activity導致Activity的資源無法被系統回收從而引起內存泄露和OOM
  • 方便代碼的維護和單元測試。

其實說了這么多,都是瞎說

Talk is cheap, let me show you the code!

三、 用mvp簡單實現一個實例

我看了很多mvp都在模擬寫一個登陸的界面,我也就來簡單的模擬一個登陸的界面吧。

activity_main的代碼:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">

    <android.support.constraint.Group
        android:id="@+id/login_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:constraint_referenced_ids="edit_username,edit_password,guide_view,login_btn,clear_btn" />

    <EditText
        android:id="@+id/edit_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="請輸入賬號"
        android:inputType="text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/edit_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:hint="請輸入密碼"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="@id/edit_username"
        app:layout_constraintTop_toBottomOf="@id/edit_username" />


    <android.support.constraint.Guideline
        android:id="@+id/guide_view"
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />

    <Button
        android:id="@+id/login_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:layout_marginTop="20dp"
        android:text="登陸"
        app:layout_constraintEnd_toStartOf="@id/guide_view"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintTop_toBottomOf="@id/edit_password" />

    <Button
        android:id="@+id/clear_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:text="重置"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@id/guide_view"
        app:layout_constraintTop_toTopOf="@id/login_btn" />

    <android.support.v4.widget.ContentLoadingProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="parent"
        app:layout_constraintEnd_toStartOf="parent"
        app:layout_constraintStart_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="parent" />

    <Button
        android:id="@+id/retry_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重試"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="parent"
        app:layout_constraintEnd_toStartOf="parent"
        app:layout_constraintStart_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="parent" />

    <TextView
        android:id="@+id/login_success_tips"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="登陸成功!!"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="parent"
        app:layout_constraintEnd_toStartOf="parent"
        app:layout_constraintStart_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="parent" />


</android.support.constraint.ConstraintLayout>

說明一下:我里面用到了很多ConstraintLayout的新屬性,如果你對這個有疑問,請翻閱我之前的文章ConstraintLayout用法詳解.

MainActivity的代碼(視圖層):

class MainActivity : AppCompatActivity(), IView, View.OnClickListener {


    private var persent: IPresent? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        persent = MainPresent(this)

        login_btn.setOnClickListener(this)

        clear_btn.setOnClickListener(this)

        retry_btn.setOnClickListener(this)

    }

    override fun onClick(view: View?) {
        when (view?.id) {
            R.id.login_btn -> {
                persent?.checkFrom(edit_username.text.toString(),
                        edit_password.text.toString())

            }
            R.id.clear_btn -> {
                edit_username.setText("")
                edit_password.setText("")
            }
            R.id.retry_btn -> {
                retry_btn.visibility = View.GONE
                persent?.checkFrom(edit_username.text.toString(),
                        edit_password.text.toString())
            }
        }
    }

    override fun errorShowTips(tips: String) {
        toast(tips)
    }

    override fun onSubmit() {
        login_group.visibility = View.INVISIBLE
        progress_bar.visibility = View.VISIBLE
    }

    override fun showResult(loginSuccess: Boolean) {
        progress_bar.visibility = View.GONE

        if (loginSuccess) {
            login_success_tips.visibility = View.VISIBLE
        } else {
            retry_btn.visibility = View.VISIBLE
        }
    }
}

MainModel的代碼(實體層):

class MainModel : IModel{

    // 模擬請求數據
    override fun login(username: String, password: String): Observable<Boolean> {
        return Observable.just(true)
    }
}

MainPresent的代碼(中介層):

class MainPresent(view: IView) : IPresent {
    private var view: IView? = null
    private var model: IModel? = null

    init {
        model = MainModel()
        this.view = view
    }

    override fun checkFrom(username: String, password: String) {
        if (username.isEmpty()) {
            view?.errorShowTips("請輸入用戶名")
            return
        }
        if (password.isBlank()) {
            view?.errorShowTips("請輸入密碼")
            return
        }
        view?.onSubmit()

        // 模擬一下網絡加載的延時
        model?.run {
            login(username = username, password = password)
                    .delay(2, TimeUnit.SECONDS)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribeBy(
                            onNext = {
                                view?.showResult(it)
                            },
                            onError = {
                                view?.showResult(false)
                            }
                    )
        }
    }
}

IFeature的代碼(封裝接口):

interface IView {
    
    fun errorShowTips(tips:String)

    fun onSubmit()

    fun showResult(loginSuccess: Boolean)

}

interface IPresent {
    
    fun checkFrom(username:String,password:String)

}

interface IModel {

    fun login(username:String,password: String): Observable<Boolean>

}

四、 重新封裝一下mvp

如果你是一個對自己的要求非常高的程序員,你會盡量去優化重復的代碼。如果你對上面的代碼已經純熟了之后,你會發現:我們每次都會寫想同的代碼。好處就是增加了你對代碼層面的熟悉程度,但是壞處就會造成大量的代碼冗余。

所以此時我們就需要一個抽取想同的代碼進行封裝操作。當然我的經歷也不算太過豐富,可能代碼考慮面沒有那么全。如果存在疑慮的呢?可以進行討論、完善。

基類:IFeature.kt

interface IModel {

    fun onAttach()

    fun onDetach()
}

interface IView

interface IPresenter<in V : IView, in M : IModel> {

    fun attach(view: V?, model: M?)

    fun onResume()

    fun onPause()

    fun detach()

    fun isAttached(): Boolean
}

Presenter:

abstract class Presenter : PresenterLifecycle, PresenterLifecycleOwner {

    protected open var mContext: Context? = null
    /**
     * mHandler is main thread handler
     */
    protected val mHandler: Handler = Handler(Looper.getMainLooper())
    /**
     * currentState is current present lifecycle state
     */
    override var currentState: Event = Event.DETACH
    /**
     * mOnAttachStateChangedListeners contains listeners object who would be notified when this presenter's lifecycle changed
     */
    private val mOnAttachStateChangedListeners: FastSafeIterableMap<OnAttachStateChangedListener, Unit> = FastSafeIterableMap()
    /**
     * isAttached is true after presenter has been invoked [onAttach]
     */
    protected var mIsAttached: Boolean = false
    /**
     * isPaused is true when presenter's lifecycle is ON_PAUSE
     */
    protected var mIsPaused: Boolean = false


    open fun onAttach(context: Context) {
        mContext = context
        mIsAttached = true
        currentState = Event.ATTACH
        synchronized(this) {
            mOnAttachStateChangedListeners.forEach { (listener, _) ->
                listener.onStateChanged(this, Event.ATTACH)
            }
        }
    }

    open fun onResume() {
        mIsPaused = false
        currentState = PresenterLifecycle.Event.ON_RESUME
        synchronized(this) {
            mOnAttachStateChangedListeners.forEach { (listener, _) ->
                listener.onStateChanged(this, Event.ON_RESUME)
            }
        }
    }

    open fun onPause() {
        mIsPaused = true
        currentState = PresenterLifecycle.Event.ON_PAUSE
        synchronized(this) {
            mOnAttachStateChangedListeners.forEach { (listener, _) ->
                listener.onStateChanged(this, Event.ON_PAUSE)
            }
        }
    }

    open fun onDetach() {
        mIsAttached = false
        currentState = PresenterLifecycle.Event.DETACH
        synchronized(this) {
            mOnAttachStateChangedListeners.forEach { (listener, _) ->
                listener.onStateChanged(this, Event.DETACH)
                mOnAttachStateChangedListeners.remove(listener)
            }
        }
    }

    override fun addOnAttachStateChangedListener(listener: PresenterLifecycle.OnAttachStateChangedListener) {
        synchronized(this) {
            mOnAttachStateChangedListeners.putIfAbsent(listener, Unit)
        }
    }

    override fun removeOnAttachStateChangesListener(listener: PresenterLifecycle.OnAttachStateChangedListener) {
        synchronized(this) {
            mOnAttachStateChangedListeners.remove(listener)
        }
    }

    override fun getLifecycle(): PresenterLifecycle {
        return this
    }

}

PresenterLifecycle

interface PresenterLifecycle {

    var currentState: Event

    fun addOnAttachStateChangedListener(listener: OnAttachStateChangedListener)

    fun removeOnAttachStateChangesListener(listener: OnAttachStateChangedListener)

    interface OnAttachStateChangedListener {
        fun onStateChanged(presenter: Presenter, event: Event)
    }

    enum class Event {
        ATTACH, ON_RESUME, ON_PAUSE, DETACH
    }
}

VMpresent:

abstract class VMPresenter<V : IView, M : IModel>(val context: Context) : Presenter(), IPresenter<V, M> {

    /**
     * viewRef is weak reference of view object
     */
    private var viewRef: WeakReference<V>? = null
    /**
     * modelRef is weak reference of model object
     */
    private var modelRef: WeakReference<M>? = null
    /**
     * Convenient property for accessing view object
     */
    protected val view: V?
        get() = viewRef?.get()
    /**
     * Convenient property for access model object
     */
    protected val model: M?
        get() = modelRef?.get()
    /**
     * isPaused is true when presenter's lifecycle is ON_PAUSE
     */
    protected val isPaused: Boolean
        get() = mIsPaused


    override fun attach(view: V?, model: M?) {
        super.onAttach(context)
        viewRef = if (view != null) WeakReference(view) else null
        modelRef = if (model != null) WeakReference(model) else null
    }

    override fun detach() {
        super.onDetach()
        // clear the listeners to avoid strong retain cycle
        modelRef = null
        viewRef = null
    }

    override fun isAttached(): Boolean = mIsAttached

}

Model:

abstract class Model(context: Context) : IModel {

    protected val context: Context = context.applicationContext

    override fun onAttach() {}

    override fun onDetach() {}

}

以上就是我自己對mvp框架的一個封裝,可能還存在著很多的漏洞。

五、 mvp的劣勢以及介紹一下mvvm

首先對于mvp的優勢,我想我就不用說了。至于mvp的劣勢:是需要加入Presenter來作為橋梁協調View和Model,同時也會導致Presenter變得很臃腫,在維護時比較不方便。而且對于每一個Activity,基本上均需要一個對應的Presenter來進行對應。

如果外加上 自己封裝的話,這種代碼的框架性就會愈發明顯。所以我覺得如果不是對邏輯有很大要求的情況之下,沒必要使用mvp框架了。

當然除了mvp框架之外,還有mvvm,甚至還有更加出色的mvpvm框架。我在這里呢?就簡單介紹一下:

  • MVVM
    MVVM其實是對MVP的一種改進,他將Presenter替換成了ViewModel,并通過雙向的數據綁定來實現視圖和數據的交互。也就是說只需要將數據和視圖綁定一次之后,那么之后當數據發生改變時就會自動的在UI上刷新而不需要我們自己進行手動刷新。在MVVM中,他盡可能的會簡化數據流的走向,使其變得更加簡潔明了。示意圖如下:


  • MVPVM

MVPVM即:Model-View-Presenter-ViewModel。此模式是MVVM和MVP模式的結合體。但是交互模式發生了比較大的變化。

Presenter同時持有View、Model、ViewModel,負責協調三方的之間的交互。

View持有ViewModel。ViewModel是View展示數據的一個映射,兩者之間雙向綁定:
(1)當View的數據發生變化時,View將數據更改同步到ViewModel。比如用戶在輸入框輸入了內容。
(2)View監聽ViewModel的數據變化,當ViewModel的數據發生變化時,View根據ViewModel的數據更新UI顯示。比如更新來自后端的數據列表。

Presenter持有View,并且View的動作響應傳遞至Presenter。當收到View的動作響應之后,Presenter通過Model獲取后端或者數據庫數據,請求參數來自于Presenter持有的ViewModel。

當Model請求到數據之后,將數據返回給Presenter,Presenter將返回的數據傳遞到ViewModel,由于View和ViewModel之間的綁定關系,View會根據ViewModel的數據更新UI顯示。


說在最后

說到項目本身呢?我是用的最新的kotlin+anko配合rx的寫法,這也是我認為我這篇文章不適合新手學習的原因。首先你能看懂這篇文章呢?可能要對kotlin有一定的了解,然后可能還需要對rx有一定的了解。這能看懂這篇文章。

至于后面的mvvm和mvpvm其實我基本上都只是有些了解,具體的我沒有進行深究,如果后面有需要 我也會深究一下這里只是做簡單的介紹罷了

我接觸kotlin也有一年多了,也寫了一個大的項目 對于這個語法有一定的心得,后續我會結合我自己的心得和體會給諸位讀者一一講述出來。好了,時間不早了,對于一個失眠的人,現在已經到極點了。先洗澡睡覺了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容