在 Android 上使用 VIPER 架构
我先是一个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 可以解决这个问题。
让我们动手解决它!
带着这些问题,我开始了一个新的 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。
这里是 “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项目,或者反过来。当然这是一个不断演化的过程,不是一尘不变的。我们非常高兴能得到你的反馈!