[Android]對MVC和MVP的總結



以下內容為原創,歡迎轉載,轉載請注明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5036289.html


經歷過的客戶端的架構分為這么幾個階段:

第一階段

使用傳統的MVC,其中的View,對應的是各種Layout布局文件,但是這些布局文件中並不像Web端那樣強大,能做的事情非常有限;Controller對應的是Activity,而Activity中卻又具有操作UI的功能,我們在實際的項目中也會有很多UI操作在這一層,也做了很多View中應該做的事情,當然Controller中也包含Controller應該做的事情,比如各種事件的派發回調,而且在一層中我們會根據事件再去調用Model層操作數據,所以這種MVC的方式在實際項目中,Activity所在的Controller是非常重的,各層次之間的耦合情況也比較嚴重,不方便單元測試。

第二階段

使用MVC的進化版——MVP,MVP中把Layout布局和Activity作為View層,增加了Presenter,Presenter層與Model層進行業務的交互,完成后再與View層交互(也就是Activity)進行回調來刷新UI。這樣一來,所有業務邏輯的工作都交給了Presenter中進行,使得View層與Model層的耦合度降低,Activity中的工作也進行了簡化。但是在實際項目中,隨着邏輯的復雜度越來越大,Activity臃腫的缺點仍然體現出來了,因為Activity中還是充滿了大量與View層無關的代碼,比如各種事件的處理派發,就如MVC中的那樣View層和Controller代碼耦合在一起無法自拔。

第三階段

也是現在正在使用的架構,針對第二階段進行優化,為了把View再次簡化,想到兩種方式:

  1. 通過使用一個Presenter代理的方式,在PresenterProxy中處理各種事件機制,View中維護一個PresenterProxy對象當然Presenter中同樣實現了真實對象Presnter所實現的接口,這樣,我們同樣在View中通過代理對象調用真實對象的代碼,結構圖如下:

  2. 為MVP增加一層專門用於處理各種的事件派發Controller層,Controller的作用僅僅是處理事件並根據事件通過維護的Presenter對象派發到對應的業務中,也就是說View層只有一個Controller的對象,View層不會主動去調用Presenter層,但是Controller層和Presenter都可能會回調到View層來刷新UI,所以層次結構就變成了如下:

現在使用的是第2種方式,使用Controller來進行對Activity中事件代碼的分離,下面使用登錄的例子來講解,其中代碼使用的並不是Java,而是Kotlin。

在演示之前,先來看下實現MVP的幾個基礎的接口和類(點這里查看AKBMVPExt.kt):

/**
 * MVP的View層,UI相關,Activity需要實現該interface
 * 它會包含一個Presenter的引用,當有事件發生(比如按鈕點擊后),會調用Presenter層的方法
 */
public interface KViewer {
    //    val onClickListener: ((view: View) -> Unit)?
    val context: Context?;

    fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
        context?.lets { Toast.makeText(this, message, duration).show() }
    }

    fun dialog(title: String? = null,
               message: String?,
               okText: String? = "OK",
               cancelText: String? = null,
               positiveClickListener: ((DialogInterface, Int) -> Unit)? = null,
               negativeClickListener: ((DialogInterface, Int) -> Unit)? = null
    ) {
        context?.lets {
            AlertDialog.Builder(this)
                    .setTitle(title)
                    .setMessage(message)
                    .setPositiveButton(okText, positiveClickListener)
                    .setNegativeButton(cancelText, negativeClickListener)
                    .show()
        }
    }

    fun showLoading(message: String) {
        Log.w(KViewer::class.java.simpleName, "loadingDialog should impl by subclass")
    }

    fun cancelLoading() {
        Log.w(KViewer::class.java.simpleName, "cancelLoadingDialog should impl by subclass")
    }

    fun <T : View> findView(resId: Int): T;

}

所有View層的Activity、Fragment或者View都要實現KViewer接口,該接口中有一個屬性和一個函數需要被子類的Activity實現:

  • context屬性:該屬性需要被子類override,該屬性用於一些接口公用的UI相關操作的方法,如toastdialogcancelDialog等。

  • fun <T : View> findView(resId: Int): T函數:該函數需要被子類Activity、Fragment或者View實現,這個方法用於從當前View層中根據id獲取到對應的View,該方法在Activity、Fragment或者View中並不一致。

當然所有的重寫都可以在BaseActivity、BaseFragment、BaseFrameLayout等中重寫,之后使用它們的子類即可。

/**
 * MVP的Presenter層,作為溝通View和Model的橋梁,它從Model層檢索數據后,返回給View層,它也可以決定與View層的交互操作。
 * 它包含一個View層的引用和一個Model層的引用
 */
public open class KPresenter<V : KViewer>(var viewer: V) {

    open public fun closeAll() {
        Log.w(KViewer::class.java.simpleName, "closeAll in KPresenter should impl by subclass")
    }

}

KPresenter類是作為所有Presenter層的實現的基類的,它只有一個closeAll函數需要被重寫,當Activity在被destory時,需要調用close函數停止到子線程的任務。

/**
 * Controller,用於派發View中的事件,它在根據不同的事件調用Presenter
 */
public abstract class KController<KV : KViewer, KP : KPresenter<*>>(val viewer: KV, presenterCreate: () -> KP) {
    protected val presenter: KP by lazy { presenterCreate() }

    private val viewCache: SparseArray<View> = SparseArray();

    /**
     * 注冊事件
     */
    abstract fun bindEvents()

    public fun <T : View> getView(resId: Int): T {
        val view: View? = viewCache.get(resId)
        return view as T? ?: viewer.findView<T>(resId).apply {
            viewCache.put(resId, this)
        }
    }

    public fun closeAll() = presenter.closeAll()
}

同樣KController是所有Controller類的基類,需要子類實現bindEvents()函數,在這個函數中,可以綁定各種View的事件。還提供了getView()方法來從Viewer中獲取到對應的控件,並且會緩存找到的控件。

一、創建BaseActivity並實現KViewer

open class BaseActivity : AppCompatActivity(), KViewer {
    override fun <T : View> findView(resId: Int): T = findViewById(resId) as T

    override val context: Context = this

    open val controller: KController<*, *>? = null

    private val loadingDialog: ProgressDialog by lazy { ProgressDialog(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 強制豎屏
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    }

    override fun showLoading(message: String) {
        loadingDialog.setMessage(message)
        loadingDialog.show()
    }

    override fun cancelLoading() {
        if (loadingDialog.isShowing) {
            loadingDialog.cancel()
        }
    }

    override fun onDestroy() {
        controller?.closeAll()
        super.onDestroy()
    }
}

這里我重寫了KViewer中的findView()函數,函數實現是通過Activity::findViewById()的方式。

又實現了controller屬性,設置為null,這個controller還需要子類再來重寫。

然后重寫了showLoadingcancelLoading,在onDestory中通過controller調用presenter中的closeAll函數。

二、實現LoginActivity

新建LoginViewer接口,繼承KViewer,並定義各種邏輯回調:

interface LoginViewer : KViewer {
    fun onLogin()
}

里面所有的函數應該都是名字onXXX的函數,都是需要去操作UI的,這里定義的是一個onLogin()函數,表示登錄成功后,我們現在是如果登錄成功后,則跳轉到主界面MainActivity

然后創建LoginActivity,實現我們的LoginViewer

class LoginActivity : BaseActivity(), LoginViewer {
    override val controller: LoginController by lazy { LoginController(this) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        controller.bindEvents()
    }
    
    override fun onLogin() {
        toActivity<MainActivity> { }
        finish()
    }
}

這里我們首先重寫父類中的controller屬性,通過lazy懶初始化LoginController,然后在onCreate中調用controllerbindEvents(),這樣,我們在controller中的bindEvents()函數中就可以對各種View進行事件的綁定,甚至包括自定義的Dialog、PopupWindow等組件的回調。

然后實現onLogin函數,在這個函數中進行界面的跳轉。

三、實現LoginController

創建LoginController,繼承KController

class LoginController(viewer: LoginViewer) : KController<LoginViewer, LoginPresenter>(viewer, { LoginPresenter(viewer) }),
        View.OnClickListener {

    override fun bindEvents() {
        getView<Button>(R.id.activity_login_submit_btn).setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.activity_login_submit_btn -> presenter.login(getView<EditText>(R.id.activity_login_username_et).text.toString(), getView<EditText>(R.id.activity_login_password_et).text.toString())

        }
    }

}

實現KController中的bindEvents函數,在bindEvents中我們通過KControllergetView()函數獲取到Id為R.id.activity_login_submit_btn的按鈕,然后設置OnClickListener,在onClick回調方法中,Controller會根據事件派發到Presenter來進行真正的登錄操作。

四、實現Presenter

創建Presenter,繼承KPresenter

class LoginPresenter(viewer: LoginViewer) : KPresenter<LoginViewer>(viewer) {
    fun login(username: String, password: String) {
        viewer.showLoading(_resString(R.string.xr_hint_logging_in))

        HttpsUrl(HttpWebApi.System.LOGIN).rxRequest {
            it.posts(
                    "username" to username,
                    "password" to password
            )
        }
                .map {
                    _gson._fromJson<LoginHttpResponse>(it.body().string()) 
                }
                .observeOnMain()
                .doOnNextOrError { viewer.cancelLoading() }
                .subscribe ({
                    if (it.success) {
                        viewer.onLogin()
                    } else {
                        showHint(it.msg)
                    }
                }) {
                    Log.e("Login", "", it)
                    viewer.toast(_resString(R.string.xr_error_default))
                }
                .bindPresenter(this)
    }
}

編寫login()函數,然后執行登錄請求,登錄成功后,通過viewer回調到View層的onLogin()函數。

如此一來,View層中只負責UI部分的工作,UI所產生的各種事件綁定、派發等職責放在Controller中,PresenterModel還是與之前一樣的職責。

關於Presenter的測試,只需mock一個LoginViewer實現類即可。

第四階段:

MVVM,把Presenter改成ViewModel,它與View之間的交互可以使用Data Binding的方式雙向進行,也就是說ViewViewModel任意一方的改變都會體現在另一方中,Google IO上提供的框架暫時還不成熟,只支持單向,所以暫時還沒有在正式的項目中使用。


實質上MV*的思想都是一樣的,解耦隔離視圖(View)和模型(Model),在實際的應用中不需要給`MVC`、`MVP`和`MVVM`一個明確的界限,甚至可以把幾者融合在一起。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM