[譯] Android 中的 MVP:如何使 Presenter 層系統化?

MVP(Model View Presenter)模式是著名的 MVC(Model View Controller)的衍生物,而且是 Android 應用程序中管理表示層的最流行的模式之一。html

這篇文章首次發表於 2014 年 4 月,從那之後就一直備受歡迎。因此我決定更新它來解決人們心中的大部分疑慮,並將代碼轉換爲 Kotlin 語言形式。前端

自那時起,架構模式發生了重大變化,例如帶有架構組件的 MVVM,但 MVP 仍然有效而且是一個值得考慮的選擇。android

什麼是 MVP 模式?

MVP 模式將 Presenter 層從邏輯中分離出來,這樣一來,就把全部關於 UI 如何工做與咱們在屏幕上如何表示它分離了開來。理想狀況下,MVP 模式將實現相同的邏輯可能具備徹底不一樣且可交替的界面。ios

要明確的第一件事是 MVP 自己不是一個架構,它只負責表示層。這是一個有爭議的說法,因此我想更深刻地解釋一下。git

你可能會發現 MVP 被定義爲架構模式,由於它能夠成爲你的應用程序架構的一部分。但你不該當這樣認爲,由於去掉 MVP 以後,你的架構依舊是完整的。MVP 僅僅塑造表示層,但若是你須要靈活且可擴展的應用程序,那麼其他層仍須要良好的體系架構。github

完整架構體系的一個示例能夠是 Clean Architecture,但還有許多其餘選擇。web

在任何狀況下,在你從未使用 MVP 的架構中去使用它老是件好事。數據庫

爲何要使用 MVP?

在 Android 開發中,咱們遇到一個嚴峻的問題:Activity 高度耦合了用戶界面和數據存取機制。咱們能夠找到像 CursorAdapter 這樣的極端例子,它將做爲視圖層一部分的 Adapter 和 屬於數據訪問層級的 Cursor 混合到了一塊兒。後端

爲了可以輕鬆地擴展和維護一個應用,咱們須要使用能夠相互分離的體系架構。若是咱們再也不從數據庫獲取數據,而是從 web 服務器獲取,那麼我接下來該怎麼辦呢?咱們可能就要從新編寫整個視圖層了。bash

MVP 使視圖獨立於咱們的數據源而存在。咱們須要將應用程序劃分爲至少三個不一樣的層次,以便咱們能夠獨立地測試它們。經過 MVP,咱們能夠將大部分有關業務邏輯的處理從 Activity 中移除,以便咱們能夠在不使用 Instrumentation Test 的狀況下對其進行測試。

如何實現 Android 當中的 MVP?

好吧,這就是它開始產生分歧的地方。MVP 有不少變種,每一個人均可以根據本身的需求和本身感受更加溫馨的方式來調整模式。這主要取決於咱們委託給 Presenter 的任務數量。

究竟是該由 View 層來負責啓用或禁用一個進度條,仍是該由 Presenter 來負責呢?又該由誰來決定 Action Bar 應該作出什麼行爲呢?這就是艱難決定的開始。我將展現我一般狀況下是如何處理這種狀況的,但我但願這篇文章更是一個適合討論的地方,而不是嚴格的約束 MVP 該如何應用,由於根本沒有「標準」的方式來實現它。

對於本文,我已經實現了一個很是簡單的示例,你能夠在個人 Github 找到 一個登陸頁面和主頁面。爲了簡單起見,本文中的代碼是使用 Kotlin 實現的,但你也能夠在倉庫中查看使用 Java 8 編寫的代碼。

Model 層

在具備完整分層體系結構的應用程序中,這裏的 Model 僅僅是通往領域層或業務邏輯層的大門。若是咱們使用 鮑勃大叔的 clean architecture 架構,這裏的 Model 多是一個實現了一個用例的 Interactor(交互器)。但就本文而言,將 Model 看作是一個給 View 層顯示數據的提供者就足夠了。

若是你檢查代碼,你將看到我建立了兩個帶有人爲延遲操做的 Interactor 來模擬對服務器的請求狀況。其中一個 Interactor 的結構:

class LoginInteractor {

    ...

    fun login(username: String, password: String, listener: OnLoginFinishedListener) {
        // Mock login. I'm creating a handler to delay the answer a couple of seconds postDelayed(2000) { when { username.isEmpty() -> listener.onUsernameError() password.isEmpty() -> listener.onPasswordError() else -> listener.onSuccess() } } } } 複製代碼

這是一個簡單的方法,它接收用戶名和密碼,並進行一些驗證操做。

View 層

View 層一般是由一個 Activity(也能夠是一個 Fragment,一個 View,這取決於 App 的結構),它包含了一個對 Presenter 的引用。理想狀況下,Presenter 是經過依賴注入的方式提供的(好比 Dagger),但若是你沒有使用這類工具,也能夠直接建立一個 Presenter 對象。View 須要作的惟一一件事就是:當有用戶操做發生時(好比一個按鈕被點擊了),就調用 Presenter 中的相應方法。

因爲 View 必須與 Presenter 層無關,所以它就須要實現一個接口。下面是示例中使用到的接口:

interface LoginView {
    fun showProgress()
    fun hideProgress()
    fun setUsernameError()
    fun setPasswordError()
    fun navigateToHome()
}
複製代碼

接口中有一些有效的方法來顯示或隱藏進度條,顯示錯誤信息,跳轉到下一個頁面等等。正如上面所提到的,有不少方式去實現這些功能,但我更喜歡羅列出最簡單直觀的方法。

而後,Activity 能夠實現這些方法。這裏我向你展現了一些用法,以便你對其用法有所瞭解:

class LoginActivity : AppCompatActivity(), LoginView {
    ...

    override fun showProgress() {
        progress.visibility = View.VISIBLE
    }

    override fun hideProgress() {
        progress.visibility = View.GONE
    }

    override fun setUsernameError() {
        username.error = getString(R.string.username_error)
    }
}
複製代碼

可是若是你還記得,我還告訴過你,View 層使用 Presenter 來通知用戶交互操做。下面就是它的用法:

class LoginActivity : AppCompatActivity(), LoginView {

    private val presenter = LoginPresenter(this, LoginInteractor())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        button.setOnClickListener { validateCredentials() }
    }

    private fun validateCredentials() {
        presenter.validateCredentials(username.text.toString(), password.text.toString())
    }

    override fun onDestroy() {
        presenter.onDestroy()
        super.onDestroy()
    }
    ...
}
複製代碼

Presenter 被定義爲 Activity 的屬性,當點擊按鈕時,它會調用 validateCredentials()方法,該方法將會通知 Presenter。

onDestroy() 方法亦是如此。咱們稍後將會看到爲何在這種狀況下須要通知 Presenter。

Presenter 層

Presenter 充當着 View 層和 Model 層的中間人。它從 Model 層獲取收據並將格式化後數據返回給 View 層。

此外,與典型的 MVC 模式不一樣的是,Presenter 決定了當你在與 View 層交互時會作何響應。所以,它將爲用戶每一個可執行的操做提供一種方法。咱們在 View 層中看到了它,這裏是代碼實現:

class LoginPresenter(var loginView: LoginView?, val loginInteractor: LoginInteractor) :
    LoginInteractor.OnLoginFinishedListener {

    fun validateCredentials(username: String, password: String) {
        loginView?.showProgress()
        loginInteractor.login(username, password, this)
    }
    ...
}
複製代碼

MVP 模式存在一些風險,經常被咱們忽略的最重要的問題是 Presenter 永遠依附在 View 上面。而且 View 層通常爲 Activity,這就意味着:

  • 咱們可能會因爲長時間的運行的任務而致使 Activity 的泄漏
  • 咱們可能會在 Activity 已經被銷燬的狀況下去更新視圖

首先,假若你可以保證可以在合理的時間內完成你的後臺任務,我將不會過於擔憂。將你的 Activity 泄漏 5-10 秒會讓你的 App 變得很糟糕,而且解決方案一般很複雜。

第二點反而更讓人擔憂。想象一下,你花費 10 秒鐘時間向服務器發送一個請求,但用戶卻在 5 秒鐘後關閉了 Activity。當回調方法正在被調用而且 UI 被更新時,App 將會崩潰,由於 Activity 正在銷燬中

爲了解決這個問題,咱們能夠在 Activity 中調用 onDestroy() 方法並清除 View:

fun onDestroy() {
    loginView = null
}
複製代碼

這樣咱們就能夠避免在任務結束時間與活動銷燬時間不一致的狀況下調用 Activity 了。

總結

在 Android 中將用戶界面層與邏輯層分離並不簡單,但 MVP 模式能夠更加輕易地防止咱們的 Activity 最終淪爲高度耦合的、包含了成百上千行代碼的類。在大型應用開發過程當中,將代碼管理好是頗有必要的。不然,對代碼的維護和擴展都會變得很困難。

現在,還有其餘的代替方案好比 MVVM,我將會創做新的文章來對 MVVM 和 MVP 作比較,並幫助開發者遷移。因此請繼續關注個人博客!

請記住 這個倉庫,你能夠在這查看 MVP 在 Kotlin 和 Java 中的代碼示例。

若是你想要了解更多關於 Kotlin 方面的內容,能夠查看個人 Kotlin for Android Developers 這本書 中的 sample 應用,或者觀看 在線課程

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索