以下內容為原創,歡迎轉載,轉載請注明
來自天天博客: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再次簡化,想到兩種方式:
-
通過使用一個Presenter代理的方式,在PresenterProxy中處理各種事件機制,View中維護一個PresenterProxy對象當然Presenter中同樣實現了真實對象Presnter所實現的接口,這樣,我們同樣在View中通過代理對象調用真實對象的代碼,結構圖如下:
-
為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相關操作的方法,如toast
、dialog
、cancelDialog
等。 -
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
還需要子類再來重寫。
然后重寫了showLoading
和cancelLoading
,在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
中調用controller
的bindEvents()
,這樣,我們在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
中我們通過KController
中getView()
函數獲取到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
中,Presenter
和Model
還是與之前一樣的職責。
關於Presenter
的測試,只需mock一個LoginViewer
實現類即可。
第四階段:
MVVM,把Presenter
改成ViewModel
,它與View
之間的交互可以使用Data Binding
的方式雙向進行,也就是說View
和ViewModel
任意一方的改變都會體現在另一方中,Google IO上提供的框架暫時還不成熟,只支持單向,所以暫時還沒有在正式的項目中使用。
實質上MV*的思想都是一樣的,解耦隔離視圖(View)和模型(Model),在實際的應用中不需要給`MVC`、`MVP`和`MVVM`一個明確的界限,甚至可以把幾者融合在一起。