[译] 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索