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

沐風同學發表於2018-12-24

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

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

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

什麼是 MVP 模式?

MVP 模式將 Presenter 層從邏輯中分離出來,這樣一來,就把所有關於 UI 如何工作與我們在螢幕上如何表示它分離了開來。理想情況下,MVP 模式將實現相同的邏輯可能具有完全不同且可交替的介面。

要明確的第一件事是 MVP 本身不是一個架構,它只負責表示層。這是一個有爭議的說法,所以我想更深入地解釋一下。

你可能會發現 MVP 被定義為架構模式,因為它可以成為你的應用程式架構的一部分。但你不應當這樣認為,因為去掉 MVP 之後,你的架構依舊是完整的。MVP 僅僅塑造表示層,但如果你需要靈活且可擴充套件的應用程式,那麼其餘層仍需要良好的體系架構。

完整架構體系的一個示例可以是 Clean Architecture,但還有許多其他選擇。

在任何情況下,在你從未使用 MVP 的架構中去使用它總是件好事。

為什麼要使用 MVP?

在 Android 開發中,我們遇到一個嚴峻的問題:Activity 高度耦合了使用者介面和資料存取機制。我們可以找到像 CursorAdapter 這樣的極端例子,它將作為檢視層一部分的 Adapter 和 屬於資料訪問層級的 Cursor 混合到了一起。

為了能夠輕鬆地擴充套件和維護一個應用,我們需要使用可以相互分離的體系架構。如果我們不再從資料庫獲取資料,而是從 web 伺服器獲取,那麼我接下來該怎麼辦呢?我們可能就要重新編寫整個檢視層了。

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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章