在 Android 上使用 VIPER 架构
jianhui_gan
8年前
<p>英文原文: <a href="/misc/goto?guid=4959746600203487828" rel="nofollow,noindex">Using the VIPER architecture on Android</a></p> <p>我先是一个Android开发者,后来也做了iOS开发,接触过几种不同的架构 - 有好有坏。</p> <p>在Android中我一直觉得MVP架构用着不错,直到在一个iOS的项目中遇到了VIPER架构,这个架构用了8个月。当我回到Android时,我决定采用这种设计,虽然有人建议说在Android上使用iOS的架构不合理,但我还想在这个平台上实现VIPER。鉴于Android 和 iOS 框架之间的基本区别,我对 VIPER 为 Android 带来的实际用处存有疑问。这样做值得吗?让我们从基本概念开始。</p> <h2>什么是 VIPER?</h2> <p>VIPER 是一个主要用在iOS开发生的简明架构。它帮助保持代码的简洁有序,避免 <a href="/misc/goto?guid=4959746600306586741" rel="nofollow,noindex">Massive-View-Controller</a> 的情况。</p> <p>VIPER 是视图 (View),交互器 (Interactor),展示器 (Presenter),实体 (Entity) 以及路由 (Routing) 的首字母缩写,各个部分都有明确的职责,遵循 <a href="/misc/goto?guid=4959746600386971850" rel="nofollow,noindex">单一职责原则</a> 。关于VIPER的更多知识,你可以查看 <a href="/misc/goto?guid=4959746600471158879" rel="nofollow,noindex">这篇不错的文章</a> 。</p> <h2>Android上的架构</h2> <p>Android上已经有一些非常不错的架构。最著名的就是 <a href="/misc/goto?guid=4959746600560137567" rel="nofollow,noindex">Model-View-ViewModel (MVVM)</a> 和 <a href="/misc/goto?guid=4958961538098926038" rel="nofollow,noindex">Model-View-Presenter (MVP)</a> 。</p> <p>如果你和 data binding 一起使用,使用MVVM就很合理,因为我不是很喜欢 data binding 的理念(ps,译者倒是很喜欢的),所以一直在项目中使用MVP。但是随着项目的增长,presenter变成了一个方法超多的庞大的类,使得它很难维护和理解。因为它要负责许多事情:处理UI事件,UI逻辑,业务逻辑,网络和数据库查询。这违背了单一职责原则,而 VIPER 可以解决这个问题。</p> <h2>让我们动手解决它!</h2> <p><img src="https://simg.open-open.com/show/f07a320355f70058982a89b4f2a26063.gif"></p> <p>带着这些问题,我开始了一个新的 Android 项目,并决定使用 MVP + Interactor (或者你也可以叫它VIPE)。这样我就可以把presenter中的某些职能移到Interactor中。 UI 事件处理以及为 View 准备来自Interactor的数据之类的事情留给presenter。然后 Interactor 只负责业务逻辑和获取来自数据库和 API 的数据。</p> <p>另外,我使用接口来将不同的module连接在一起。这样不同模块之间的方法就互不干扰,有助于清晰的定义每个模块的职责,避免程序员把逻辑放错了地方。下面是接口的定义:</p> <pre> <code class="language-java">/*** 本文源码为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) } }</code></pre> <p>下面是实现了这些接口的类的代码( <a href="/misc/goto?guid=4958868881467951527" rel="nofollow,noindex">Kotlin</a> 写的,但是Java类似)。</p> <pre> <code class="language-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!") }) } }</code></pre> <p>完整的代码在 <a href="/misc/goto?guid=4959746600699745793" rel="nofollow,noindex">this Gist</a> 。</p> <p>你可以看到modules是在开始的时候被创建和连接在一起的。当创建Activity的时候,它初始化了Presenter,把自己作为一个View传递给Presenter的构造函数。然后这个Presenter把自己作为InteractorOutput初始化Interactor。</p> <p>而在一个iOS VIPER 项目中这应该是由Router来做的,创建UIViewController,或者从一个Storyboard获得它,然后把所有的module写在一起。但是在 Android 中我们不是自己创建Activity,而是通过Intent,我们无法从前一个Activity获取新建的Activity。这有助于避免内存泄漏,但是如果你想传递数据到新的模块就有点痛苦了。我们还不能把Presenter放到Intent的extra中,因为它需要是Parcelable 或者 Serializable的。</p> <p>这就是为什么这个项目中我省略了Router。但是这是最佳选择吗?</p> <h2>VIPE + Router</h2> <p>前面VIPE的实现解决了MVP的绝大多数问题,用Interactor分离Presenter的职责。</p> <p>但是,View并不像 iOS VIPER的View那样被动。它需要处理所有的常规职责以及导航到其它模块。这并不是它的工作,我们可以做的更好。我们要引入Router。</p> <p><img src="https://simg.open-open.com/show/951f4198055bdba33cbb98285ef7214e.gif"></p> <p>这里是 “VIPE” 和 VIPER之间的不同之处:</p> <pre> <code class="language-java">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) } }</code></pre> <p>完整的代码在 <a href="/misc/goto?guid=4959746600785753840" rel="nofollow,noindex">这里</a> 。</p> <p>现在我们把view的 routing 逻辑放到Router中。它只需要一个 Activity 的实例来调用 startActivity 方法。虽然我们仍然没有像iOS VIPER 那样把所有的东西都捆在一起,但至少遵循了单一职责原则。</p> <h2>总结</h2> <p>在使用MVP + Interactor开发了一个项目,以及帮助一个同事开发了一个完全的VIPER Android 项目之后,我可以负责人的说这个架构在 Android 上是可行的,也值得这样去做。类变得更小更易维护。它还影响了开发进度,因为这个架构明确的告诉了你代码该写在什么地方。</p> <p>在我司Cheesecake Lab,我们决定在大部分新项目中使用VIPER。这样可以更高的维护项目,而且更易从一个iOS项目切换到Android项目,或者反过来。当然这是一个不断演化的过程,不是一尘不变的。我们非常高兴能得到你的反馈!</p> <p> </p> <p>来自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0329/7754.html</p> <p> </p>