在 Android 上使用 VIPER 架构

泡在网上的日子 / 文 发表于2017-03-29 16:09 次阅读 架构,VIPER

英文原文:Using the VIPER architecture on Android 

我先是一个Android开发者,后来也做了iOS开发,接触过几种不同的架构 - 有好有坏。

在Android中我一直觉得MVP架构用着不错,直到在一个iOS的项目中遇到了VIPER架构,这个架构用了8个月。当我回到Android时,我决定采用这种设计,虽然有人建议说在Android上使用iOS的架构也许不合理,但我还是想在这个平台上实现VIPER。鉴于Android 和 iOS 框架之间的基本区别,我对 VIPER 为 Android 带来的实际用处存有疑问。这样做值得吗?让我们从基本概念开始。

什么是 VIPER?

VIPER 是一个主要用在iOS开发生的简明架构。它帮助保持代码的简洁有序,避免Massive-View-Controller的情况。

VIPER 是视图 (View),交互器 (Interactor),展示器 (Presenter),实体 (Entity) 以及路由 (Routing) 的首字母缩写,各个部分都有明确的职责,遵循单一职责原则。关于VIPER的更多知识,你可以查看这篇不错的文章

Android上的架构

Android上已经有一些非常不错的架构。最著名的就是  Model-View-ViewModel (MVVM) 和 Model-View-Presenter (MVP)

如果你和 data binding 一起使用,使用MVVM就很合理,因为我不是很喜欢 data binding 的理念(ps,译者倒是很喜欢的),所以一直在项目中使用MVP。但是随着项目的增长,presenter变成了一个方法超多的庞大的类,使得它很难维护和理解。因为它要负责许多事情:处理UI事件,UI逻辑,业务逻辑,网络和数据库查询。这违背了单一职责原则,而 VIPER 可以解决这个问题。

让我们动手解决它!

giphy-11.gif

带着这些问题,我开始了一个新的 Android 项目,并决定使用 MVP + Interactor (或者你也可以叫它VIPE)。这样我就可以把presenter中的某些职能移到Interactor中。 UI 事件处理以及为 View 准备来自Interactor的数据之类的事情留给presenter。然后 Interactor 只负责业务逻辑和获取来自数据库和 API 的数据。

另外,我使用接口来将不同的module连接在一起。这样不同模块之间的方法就互不干扰,有助于清晰的定义每个模块的职责,避免程序员把逻辑放错了地方。下面是接口的定义:

/*** 本文源码为Kotlin ***/

class LoginContracts {
  interface View {
    fun goToHomeScreen(user: User)
    fun showError(message: String)
  }

  interface Presenter {
    fun onDestroy()
    fun onLoginButtonPressed(username: String, password: String)
  }

  interface Interactor {
    fun login(username: String, password: String)
  }

  interface InteractorOutput {
    fun onLoginSuccess(user: User)
    fun onLoginError(message: String)
  }
}

下面是实现了这些接口的类的代码(Kotlin写的,但是Java类似)。

class LoginActivity: BaseActivity, LoginContracts.View {

  var presenter: LoginContracts.Presenter? = LoginPresenter(this)

  override fun onCreate() {
    //...
    loginButton.setOnClickListener { onLoginButtonClicked() }
  }

  override fun onDestroy() {
    presenter?.onDestroy()
    presenter = null
    super.onDestroy()
  }

  private fun onLoginButtonClicked() {
    presenter?.onLoginButtonClicked(usernameEditText.text, passwordEditText.text)
  }

  fun goToHomeScreen(user: User) {
    val intent = Intent(view, HomeActivity::class.java)
    intent.putExtra(Constants.IntentExtras.USER, user)
    startActivity(intent)
  }

  fun showError(message: String) {
    //shows the error on a dialog
  }
}

class LoginPresenter(var view: LoginContract.View?): LoginContract.Presenter, LoginContract.InteractorOutput {
    var interactor: LoginContract.Interactor? = LoginInteractor(this)

    fun onDestroy() {
      view = null
      interactor = null
    }

    fun onLoginButtonPressed(username: String, password: String) {
      interactor?.login(username, password)
    }

    fun onLoginSuccess(user: User) {
      view?.goToNextScreen(user)
    }

    fun onLoginError(message: String) {
      view?.showError(message)
    }
}

class LoginInteractor(var output: LoginContract.InteractorOutput?): LoginContract.Interactor {
  fun login(username: String, password: String) {
    LoginApiManager.login(username, password)
                ?.subscribeOn(Schedulers.newThread())
                ?.observeOn(AndroidSchedulers.mainThread())
                ?.subscribe({
                          //does something with the user, like saving it or the token
                          output?.onLoginSuccess(it)
                          },
                        { output?.onLoginError(it.message ?: "Error!") })
  }
}

完整的代码在this Gist

你可以看到modules是在开始的时候被创建和连接在一起的。当创建Activity的时候,它初始化了Presenter,把自己作为一个View传递给Presenter的构造函数。然后这个Presenter把自己作为InteractorOutput初始化Interactor。

而在一个iOS VIPER 项目中这应该是由Router来做的,创建UIViewController,或者从一个Storyboard获得它,然后把所有的module写在一起。但是在 Android 中我们不是自己创建Activity,而是通过Intent,我们无法从前一个Activity获取新建的Activity。这有助于避免内存泄漏,但是如果你想传递数据到新的模块就有点痛苦了。我们还不能把Presenter放到Intent的extra中,因为它需要是Parcelable 或者 Serializable的。

这就是为什么这个项目中我省略了Router。但是这是最佳选择吗?

VIPE + Router

前面VIPE的实现解决了MVP的绝大多数问题,用Interactor分离Presenter的职责。

但是,View并不像 iOS VIPER的View那样被动。它需要处理所有的常规职责以及导航到其它模块。这并不是它的工作,我们可以做的更好。我们要引入Router。

giphy1.gif

这里是 “VIPE” 和 VIPER之间的不同之处:

class LoginContracts {
  interface View {
    fun showError(message: String)
    //fun goToHomeScreen(user: User)     //这不再是View的职责的一部分 
  }

  interface Router {
    fun goToHomeScreen(user: User) // 现在由router来处理它 
  }
}

class LoginPresenter(var view: LoginContract.View?): LoginContract.Presenter, LoginContract.InteractorOutput {
    //now the presenter has a instance of the Router and passes the Activity to it on the constructor
    var router: LoginContract.Router? = LoginRouter(view as? Actiity)

    //...

    fun onLoginSuccess(user: User) {
      router?.goToNextScreen(user)
    }

    //...
}

class LoginRouter(var activity: Activity?) {
  fun goToHomeScreen(user: User) {
    val intent = Intent(view, HomeActivity::class.java)
    intent.putExtra(Constants.IntentExtras.USER, user)
    activity?.startActivity(intent)
  }
}

完整的代码在这里

现在我们把view的 routing 逻辑放到Router中。它只需要一个 Activity 的实例来调用 startActivity 方法。虽然我们仍然没有像iOS VIPER 那样把所有的东西都捆在一起,但至少遵循了单一职责原则。

总结

在使用MVP + Interactor开发了一个项目,以及帮助一个同事开发了一个完全的VIPER Android 项目之后,我可以负责人的说这个架构在 Android 上是可行的,也值得这样去做。类变得更小更易维护。它还影响了开发进度,因为这个架构明确的告诉了你代码该写在什么地方。

在我司Cheesecake Lab,我们决定在大部分新项目中使用VIPER。这样可以更高的维护项目,而且更易从一个iOS项目切换到Android项目,或者反过来。当然这是一个不断演化的过程,不是一尘不变的。我们非常高兴能得到你的反馈!


收藏 赞 (0) 踩 (0)
上一篇:基于MVP架构和Material Design设计风格,采用RxJava2, Retrofit2, Realm and ZXing开发的快递查询App
Espresso Android App Espresso is an express delivery tracking app designed with Material Design style, built on MVP(Model-View-Presenter) architecture with RxJava2, Retrofit2, Realm database and ZXing. The source code in this repository re
下一篇:Android ReactNative bundle拆分合成方案
目前大多数的 APP 对于 React Native 都是一个尝试阶段,用混合开发的方式,在应用中用 React Native 去实现个别或则是几个页面。 首先我们来看一下在一个 App 中嵌入 RN 页面的主要的类图以及相互之间的关系 BaseReactActivity 用于加载业务 js bundle 文件