在 Android 上使用 VIPER 架构

jianhui_gan 7年前
   <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>