天天看点

[译] MVVM, Coordinators 和 RxSwift 的抽丝剥茧

<b>本文讲的是[译] MVVM, Coordinators 和 RxSwift 的抽丝剥茧,</b>

<b></b>

去年,我们的团队开始在生产应用中使用 Coordinators 和 MVVM。 起初看起来很可怕,但是从那时起到现在,我们已经完成了 4 个基于这种模式开发的应用程序。在本文中,我将分享我们的经验,并将指导你探索 MVVM, Coordinators 和响应式编程。

我们将从一个简单的 MVC 示例应用程序开始,而不是一开始就给出一个定义。我们将逐步进行重构,以显示每个组件如何影响代码库以及结果如何。每一步都将以简短的理论介绍作为前提。

在这篇文章中,我们将使用一个简单的示例程序,这个程序展示了 GitHub 上不同开发语言获得星数最多的库列表,并把这些库以星数多少进行排序。包含两个页面,一个是通过开发语言种类进行筛选的库列表,另一个则是用来分类的开发语言列表。

Screens of the example app

用户可以通过点击导航栏上的按钮来进入第二个页面。在这个开发语言列表里,可以选择一个语言或者通过点击取消按钮来退出页面。如果用户在第二个页面选择了一个开发语言,页面将会执行退出操作,而仓库列表页面也会根据已选的开发语言来进行内容刷新。

你可以在下面的链接里找到源代码文件:

<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fuptechteam%2FCoordinator-MVVM-Rx-Example" target="_blank"></a>

大部分的代码都在两个视图控制器中:<code>RepositoryListViewController</code> 和<code>LanguageListViewController</code>。第一个视图控制器获取了一个最受欢迎仓库的列表,然后通过表格展示给了用户,第二个视图控制器则是展示了一个开发语言的列表。<code>RepositoryListViewController</code> 是 <code>LanguageListViewController</code> 的一个代理持有对象,遵循下面的协议:

<code>RepositoryListViewController</code> 也是列表视图的代理持有对象和数据源持有对象。它处理导航事件,格式化可展示的 Model 数据以及执行网络请求。哇哦,一个视图控制器包揽了这么多的责任。

The <code>RepositoryListViewController</code> is also a delegate and a data source for the table view. It handles the navigation, formats model data to display and performs network requests. Wow, a lot of responsibilities for just one View Controller!

另外,你可以注意到 <code>RepositoryListViewController</code> 这个文件的全局范围内有两个变量:<code>currentLanguage</code> 和 <code>repositories</code>。这种状态变量使得类变得复杂了起来,而如果应用出现了意料之外的崩溃,这也会是一种常见的 BUGS 来源。总而言之,当前的代码中存在着好几个问题:

视图控制器包揽了太多的责任;

我们需要被动地处理状态的变化;

代码不可测。

是时候去见一下我们新的客人了。

这个组件将允许我们被动的响应状态变化和写出声明式代码。

Rx 是什么?其中有一个定义是这样的:

ReactiveX 是一个通过使用可观察的序列来组合异步事件编码的类库。

代理模式完成

<code>LanguageListViewControllerDelegate</code> 变成了 <code>didSelectLanguage</code> 和 <code>didCancel</code> 两个对象。我们在 <code>prepareLanguageListViewController(_: )</code> 方法中使用这两个对象来被动的观察<code>RepositoryListViewController</code> 事件。

接下来,我们将重构 <code>GithubService</code> 来返回观察对象以取代回调 block 的使用。在那之后,我们将使用 RxCocoa 框架来重写我们的视图控制器。<code>RepositoryListViewController</code> 的大部分代码将会被移动到 <code>setupBindings</code> 方法,在这个方法里面我们来声明视图控制器的逻辑。

视图控制器逻辑的声明性描述

现在我们可以不用在视图控制器里面实现列表视图的代理对象方法和数据源对象方法了,也将我们的状态变化更改成一种可变的主题。

我们已经使用 RxSwift 和 RxCocoa 框架来重构了示例应用。所以这种写法到底给我们带来了什么好处呢?

所有逻辑都是被声明式地写到了同一个地方。

我们通过观察和响应的方式来处理状态的变化。

我们使用 RxCocoa 的语法糖来简短明了地设置列表视图的数据源和代理。

我们的代码仍然不可测试,而视图控制器也还是有着很多的逻辑处理。让我们来看看我们的架构的下一个组成部分。

MVVM 是 Model-View-X 系列的 UI 架构模式。MVVM 与标准 MVC 类似,除了它定义了一个新的组件 - ViewModel,它允许更好地将 UI 与模型分离。本质上,ViewModel 是独立表现视图 UIKit 的对象。

首先,让我们创建一个 View Model,它将准备在 View 中显示的 Model 数据:

接下来,我们将把所有的数据变量和格式代码从 <code>RepositoryListViewController</code> 移动到<code>RepositoryListViewModel</code>:

现在,我们的视图控制器将所有 UI 交互(如按钮点击或行选择)委托给 View Model,并观察 View Model 输出数据或事件(像 <code>showLanguageList</code> 这样)。

我们将为 <code>LanguageListViewController</code> 做同样的事情,看起来一切进展顺利。但是我们的测试文件夹仍然是空的!View Models 的引入使我们能够测试一大堆代码。因为 ViewModels 纯粹地使用注入的依赖关系将输入转换为输出。ViewModels 和单元测试是我们应用程序中最好的朋友。

我们将使用 RxSwift 附带的 RxTest 框架测试应用程序。最重要的部分是 <code>TestScheduler</code> 类,它允许你通过定义在何时应该发出值来创建假的可观察值。这就是我们测试 View Models 的方式:

好啦,我们已经从 MVC 转到了 MVVM。 但是两者有什么区别呢?

视图控制器更轻量化;

数据处理的逻辑与视图控制器分离;

MVVM 使我们的代码可以测试;

我们的 View Controllers 还有一个问题 - <code>RepositoryListViewController</code> 知道<code>LanguageListViewController</code> 的存在并且管理着导航流。让我们用 Coordinators 来解决它。

简而言之,Coordinators 是控制我们应用程序的导航流的对象。 他们帮助的有:

解耦和重用 ViewControllers;

将依赖关系传递给导航层次;

定义应用程序的用例;

实现深度链接;

Coordinators 流程

该图显示了应用程序中典型的 coordinators 流程。App Coordinator 检查是否存在有效的访问令牌,并决定显示下一个 coordinator - 登录或 Tab Bar。TabBar Coordinator 显示三个子 coordinators,它们分别对应于 Tab Bar items。

首先,我们来看看 <code>BaseCoordinator</code> 是什么:

基本 Coordinator

该通用对象为具体 coordinators 提供了三个功能:

启动 coordinator 工作(即呈现视图控制器)的抽象方法 <code>start()</code> ;

在通过的子 coordinator 上调用 <code>start()</code> 并将其保存在内存中的通用方法 <code>coordinate(to: )</code>;

被子类使用的 <code>disposeBag</code>;

为什么 <code>*start*</code> 方法返回一个 <code>*Observable*</code>,什么又是 <code>*ResultType*</code>* 呢?

<code>ResultType</code> 是表示 coordinator 工作结果的类型。更多的 <code>ResultType</code> 将是 <code>Void</code>,但在某些情况下,它将会是可能的结果情况的枚举。<code>start</code> 将只发出一个结果项并完成。

我们在应用程序中有三个 Coordinators:

Coordinators 层级结构的根 <code>AppCoordinator</code>;

RepositoryListCoordinator`;

<code>LanguageListCoordinator</code>。

让我们看看最后一个 Coordinator 如何与 ViewController 和 ViewModel 进行通信,并处理导航流程:

LanguageListCoordinator 工作的结果可以是选定的语言,如果用户点击了“取消”按钮,也可以是无效的。这两种情况都在 <code>LanguageListCoordinationResult</code> 枚举中被定义。

在 <code>RepositoryListCoordinator</code> 中,我们通过 <code>LanguageListCoordinator</code> 的显示来绘制<code>showLanguageList</code> 的输出。在 <code>LanguageListCoordinator</code> 的 <code>start()</code> 方法完成后,我们会过滤结果,如果有一门语言被选中了,我们就将其作为参数来调用 View Model 的<code>setCurrentLanguage</code> 方法。

注意我们返回了 <code>*Observable.never()*</code> 因为仓库列表的页面一直都是在视图栈级结构里面的。

我们完成了我们最后一步的重构,我们做了:

把导航栏的逻辑移除出了视图控制器,进行了解耦;

将视图模型注入到视图控制器中;

简化了故事板;

以鸟瞰图的方式,我们的系统是长这样子的:

MVVM-C 架构设计

应用的 Coordinator 管理器启动了第一个 Coordinator 来初始化 View Model,然后注入到了视图控制器并进行了展示。视图控制器发送了类似按钮点击和 cell section 这样的用户事件到 View Model。而 View Model 则提供了处理过的数据回到视图控制器,并且调用 Coordinator 来进入下一个页面。当然,Coordinator 也可以传送事件到 View Model 进行处理。

我们已经考虑到了很多:我们讨论的 MVVM 对 UI 结构进行了描述,使用 Coordinators 解决了导航/路由的问题,并且使用 RxSwift 对代码进行了声明式改造。我们一步步的对应用进行了重构,并且展示了每一步操作的影响。

构建一个应用是没有捷径的。每一个解决方案都有其自身的缺点,不一定都适用于你的应用。进行应用结构的选择,重点在于特定情况的权衡利弊。

当然,相比之前而言,Rx,Coordinators 和 MVVM 相互结合的方式有更多的使用场景,所以请一定要让我知道,如果你希望我写多一篇更深入边界条件,疑难解答的博客的话。

感谢你的阅读!

<b>原文发布时间为:2017年9月4日</b>

<b>本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。</b>

继续阅读