RxFlow 2: 实践

文章目录
  1. 1. 都是关于 States
  2. 2. 使用 Flow
  3. 3. 导航是一个副作用
  4. 4. Navigating 是为了生成 NextFlowItems(FlowContributors)
  5. 5. 为什么对与一个 Flow & Step 组合产生多个 NextFlowItems (FlowContributors)是可以的?
  6. 6. 为什么对于Flow和Step的组合完全不产生NextFlowItem(FlowContributors),为什么可以?
  7. 7. 在 Flow 中会发生什么……停留在 Flow 中!
  8. 8. 依赖注入变得容易
  9. 9. 如何引导导航过程
  10. 10. 奖励

注意!!!

1
public typealias NextFlowItems = FlowContributors

原文RxFlow Part 2: In Practice
-

上篇:RxFlow-1-原理

几周前我介绍了 RxFlow 框架,我已经在这个框架上工作了几个月,现在可以使用了。如果您尚未阅读,建议您看一下这篇文章

总结,RxFlow 旨在:

  • 轻松的将你的导航切成逻辑部分
  • 把导航代码从 ViewController 中删除
  • 鼓励 ViewController 复用性
  • 促进 响应式编程
  • 促进 依赖注入

快速回忆下以下术语:

  • Flow: 每个 Flow 都在应用程序中定义了一个导航区域。
  • Step: 在应用中,每个 Step 就是一个导航状态,Flows 和 Steps 的结合描述了有所导航操作的可能。
  • Stepper: 任何可以发出 Steps 的东西。Steppers 负责触发每个在 Flows 中的导航操作
  • Presentable: 可以被呈现出来的事物的抽象(基本上是 UIViewController 和 可以呈现的 Flow)
  • NextFlowItem(FlowContributors): 他会告诉 Coordinator,在他的响应机制中,接下来将会是什么产生新的 Steps
  • Coordinator: Coordinator 的工作是以一种一直的方式混合Flows and Steps组合

同样重要的是要记住,RxFlow使用面向协议的程序设计,这样它才不会将代码冻结在继承层次结构中。

RxFlow repo 中,你将找到一个演示应用程序。它几乎显示了每种可能的导航类型

  • Navigation stack
  • Tab bar
  • Master / detail
  • Modal popup

-

都是关于 States

RxFlow 主要是使用响应式的方式处理导航状态。为了在多个上下文中复用,这些状态一定要不知道当前使用的导航 Flow。因此,状态不是表示“我要跳转到此屏幕”,而是表示“某人或某物执行此操作”,然后RxFlow会根据当前导航 Flow 选择正确的screen。对于 RxFlow,这个导航状态称为“Steps”。

枚举是描述 Steps 的好方法:

  • 枚举方便使用
  • 一个 value 只可以被定义一次(因此一个状态是惟一的)
  • 枚举可以安全使用,因为Swift在 switch 语法中要求你实现所有可能值
  • 枚举可以关联 value,这些value 可以从一个 screen 传到另一个 screen
  • 枚举是值类型,因此不存在传递不受控制的共享参考
eg: 在 demo App 中,这些都是我们涵盖导航可能性所需的所有 Steps。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RxFlow
enum DemoStep: Step {
case apiKey
case apiKeyIsComplete

case movieList

case moviePicked (withMovieId: Int)
case castPicked (withCastId: Int)

case settings
case settingsDone
case about
}

使用 Flow

对于 RxFlow,所有导航代码都在 Flow 中声明,eg:presenting 或者 pushing ViewController。在你的 App 中一个 Flow 代表了导航逻辑段,当 Flow 和一个明确的 Step 结合后,Flow触发导航动作。

为此,Flow 要实现:

  • 一个 “navigate(to:)” 方法根据 Flow 和 Step,执行导航操作
  • 一个 “root” UIViewController,他将基于在此 Flow 中的导航

有个 Flow 控制 UINavigationController 和 stack的例子。

在这个 Flow 中,可以执行3个导航操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import RxFlow
import UIKit

class WatchedFlow: Flow {

var root: UIViewController {
return self.rootViewController
}

private let rootViewController = UINavigationController()
private let service: MoviesService

init(withService service: MoviesService) {
self.service = service
}

func navigate(to step: Step) -> [NextFlowItem] /*FlowContributors*/ {
guard let step = step as? DemoStep else { return NextFlowItem.noNavigation }

switch step {

case .movieList:
return navigateToMovieListScreen()
case .moviePicked(let movieId):
return navigateToMovieDetailScreen(with: movieId)
case .castPicked(let castId):
return navigateToCastDetailScreen(with: castId)
default:
return NextFlowItem.noNavigation
}
}

private func navigateToMovieListScreen () -> [NextFlowItem]/*FlowContributors*/ {
let viewModel = WatchedViewModel(with: self.service)
let viewController = WatchedViewController.instantiate(with: viewModel)
viewController.title = "Watched"
self.rootViewController.pushViewController(viewController, animated: true)
return [NextFlowItem(nextPresentable: viewController, nextStepper: viewModel)]
}

private func navigateToMovieDetailScreen (with movieId: Int) -> [NextFlowItem]/*FlowContributors*/ {
let viewModel = MovieDetailViewModel(withService: self.service,
andMovieId: movieId)
let viewController = MovieDetailViewController.instantiate(with: viewModel)
viewController.title = viewModel.title
self.rootViewController.pushViewController(viewController, animated: true)
return [NextFlowItem(nextPresentable: viewController, nextStepper: viewModel)]
}

private func navigateToCastDetailScreen (with castId: Int) -> [NextFlowItem]/*FlowContributors*/ {
let viewModel = CastDetailViewModel(withService: self.service,
andCastId: castId)
let viewController = CastDetailViewController.instantiate(with: viewModel)
viewController.title = viewModel.name
self.rootViewController.pushViewController(viewController, animated: true)
return NextFlowItem.noNavigation
}
}

导航是一个副作用

在学习函数响应式编程的时候,我们经常读到 副作用。FRP 的目的是传递事件,然后再整过程中使用 FRP 的函数处理事件。这些functions 可以转换事件,最终(但是不是必须)将执行你想要的任何功能的代码(网络请求,保存文件,显示一个 alert……):这些是 副作用

因为 RxFlow 依赖响应式编程,我们可以轻松地识别出固有的概念:

  • events:发出 Steps
  • function:就是 “navigate(to:)” function
  • transformation:“navigate(to:)” 将 Step 转换成另一个 NextFlowItem (FlowContributors)
  • side effects:执行在“navigate(to:)”中的导航操作(eg: “navigateToMovieListScreen()” 方法在navigation stack 上 push 一个新的UIViewController)

接着来说,一个 NextFlowItem(FlowContributors) 是一个持有一个 Presentable 和一个 Stepper 简单的数据结构。

  • Presentable 告诉 Coordinator 接下来你要 present 出来的是什么
  • Stepper 告诉 Coordinator 接下来是什么东西发出 Steps

默认情况,所有的 UIViewController 都是 Presentable. Flows 也是 Presentable,因为有事,你想启动一个新的导航区域,该区域在他自己的 Flow 中描述,所以 RxFlow 也会把他当做可以被 present 的东西

为什么 Coordinator 要知道 Presentables

Presentable 是一个描述可以被呈现的事物的抽象类。因为 Step 不会被发出,除非他关联的 Presentable 被显示,Presentable 提供 Observables,Coordinator 会订阅这个 Observables(所以 Coordinator 会知道 Presentable 的显示状态)。因此 Presentable 没有完全显示的时候发送一个 Step 不存在任何危险。

Stepper 可以是任何东西:自定义的 UIViewController,ViewModel,Presenter…… 一旦他在 Coordinator 中注册,Stepper 就可以通过他的 ”step" 属性发送 Steps(step 是 RxSwift 中的 subject)。 Coordinator 会监听 Stepper 发送出来的 Steps,调用 Flow’s “navigate(to:)”

在 demo App 中有一个 Stepper 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import RxFlow
import RxSwift

class WatchedViewModel: Stepper {

let movies: [MovieViewModel]

init(with service: MoviesService) {
// we can do some data refactoring in order to display
// things exactly the way we want (this is the aim of a ViewModel)
self.movies = service.watchedMovies().map({ (movie) -> MovieViewModel in
return MovieViewModel(id: movie.id,
title: movie.title,
image: movie.image)
})
}

public func pick (movieId: Int) {
self.step.onNext(DemoStep.moviePicked(withMovieId: movieId))
}
}

在这个例子中,当用户选择一个 movie的时候会调用 pick 函数。这个函数在 “self.step” Rx Stream 中发出一个new value。

总结导航过程:

  • navigate(to:) 函数调用时,传入一个 Step 作为参数
  • 根据这个 Step,一些导航代码被调用(side effects
  • 也根据这个 Step,产生 NextFlowItems (FlowContributors)。因此,PresentablesSteppers 被注册进 Coordinator
  • Steppers 发出新的 Steps,然后再来一次以上过程

为什么对与一个 Flow & Step 组合产生多个 NextFlowItems (FlowContributors)是可以的?

因为在某一个时间没有什么禁止一个app有多个导航。eg: tab bar上面的每一个item 多会导向一个 navigation stack。Step 触发UITabbarController 显示,将在每个navigation stack 中生成一个 NextFlowItem (FlowContributors)。

你可以看一下 demo app 理解一下概念。这里有我们把一个 UITabbarController 和 2个Flows连接的一段代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private func navigationToDashboardScreen () -> [NextFlowItem] /*(FlowContributors)*/ {
let tabbarController = UITabBarController()
let wishlistStepper = WishlistStepper()
let wishListFlow = WishlistWarp(withService: self.service,
andStepper: wishlistStepper)
let watchedFlow = WatchedFlow(withService: self.service)

Flows.whenReady(flow1: wishListFlow, flow2: watchedFlow, block: { [unowned self]
(root1: UINavigationController, root2: UINavigationController) in
let tabBarItem1 = UITabBarItem(title: "Wishlist",
image: UIImage(named: "wishlist"),
selectedImage: nil)
let tabBarItem2 = UITabBarItem(title: "Watched",
image: UIImage(named: "watched"),
selectedImage: nil)
root1.tabBarItem = tabBarItem1
root1.title = "Wishlist"
root2.tabBarItem = tabBarItem2
root2.title = "Watched"

tabbarController.setViewControllers([root1, root2], animated: false)
self.rootViewController.pushViewController(tabbarController, animated: true)
})

return ([NextFlowItem(nextPresentable: wishListFlow,
nextStepper: wishlistStepper),
NextFlowItem(nextPresentable: watchedFlow,
nextStepper: OneStepper(withSingleStep: DemoStep.movieList))])
}

静态方法 “Flows.whenReady()” 带着参数 Flows 启动还带有一个闭包,当 Flows 准备显示的时候回调(即当Flow的第一个屏幕被选中的时候)

为什么对于Flow和Step的组合完全不产生NextFlowItem(FlowContributors),为什么可以?

因为导航 Flow 必须要有一个终点!eg 导航stack的最后一个屏幕不会再向下导航,他只可以 pop back。在这种情况,“navigate(to:)” 返回 NextFlowItem.noNavigation

在 Flow 中会发生什么……停留在 Flow 中!

正如我们已经看到的,在同一时间有多个 Flows 被导航是可以的。eg:在 navigation stack 中的一个 screen 可以启动弹出窗口,该弹出窗口也可以包含另一个navigation stack。从 UIKit 的角度上看,UIViewController的层级结构非常重要,我们不能弄混 Coordinator 内部的层次结构。

这就是为什么当一个 Flow 没有显示(在我们的例子中,就是当第一个 navigation stack 在弹出窗口之下),Coordinator将忽视 Flow 中可能发出来的 Steps

从更一般的角度来看,在 Flow 上下文中发出的 Steps 只能在该 Flow 上下文中解释(它们不能被其他 Flow 捕获)。

依赖注入变得容易

DI 是RxFlow的一个主要目标。基本上,依赖注入可以通过将某种实现(服务,管理器等)作为参数传递给初始化程序或方法来完成(也可以通过属性来完成)。

RxFlow 中, 开发人员负责实例化UIViewControllers,ViewModels,Presenter等,这是一个注入你所需代码的绝佳机会。

下面是ViewModel中依赖项注入的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import RxFlow
import UIKit

class WatchedFlow: Flow {
...
private let service: MoviesService

init(withService service: MoviesService) {
self.service = service
}
...
private func navigateToMovieListScreen () -> [NextFlowItem] /*(FlowContributors)*/ {
// inject Service into ViewModel
let viewModel = WatchedViewModel(with: self.service)

// injecy ViewMNodel into UIViewController
let viewController = WatchedViewController.instantiate(with: viewModel)

viewController.title = "Watched"
self.rootViewController.pushViewController(viewController, animated: true)
return [NextFlowItem(nextPresentable: viewController, nextStepper: viewModel)]
}
...
}

如何引导导航过程

既然已经知道如何将事物组合在一起,将 FlowsSteps 混合在一起以触发导航动作并产生NextFlowItems (FlowContributors),剩下要做的一件事:在应用程序启动时引导导航过程。

一切都在AppDelegate中发生,并且会发现这非常简单:

  • 实例化 Coordinator
  • 实例化要导航的第一个 Flow
  • Coordinator 使用第一个 Step 来调度 Flow
  • 当第一个 Flow 准本好了,把他的 root 配置成 Window的 rootViewController
在 demo App 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import UIKit
import RxFlow
import RxSwift
import RxCocoa

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

let disposeBag = DisposeBag()
var window: UIWindow?
var coordinator = Coordinator()
let movieService = MoviesService()
lazy var mainFlow = {
return MainFlow(with: self.movieService)
}()

func application(_ application: UIApplication,
didFinishWithOptions options: [UIApplicationLaunchOptionsKey: Any]?)
-> Bool {

guard let window = self.window else { return false }

Flows.whenReady(flow: mainFlow, block: { [unowned window] (root) in
window.rootViewController = root
})

coordinator.coordinate(flow: mainFlow,
withStepper: OneStepper(withSingleStep: DemoStep.apiKey))

return true
}
}

奖励

协调器有两个响应式扩展:willNavigate和didNavigate。例如,你可以在AppDelegate中订阅它们。

1
2
3
coordinator.rx.didNavigate.subscribe(onNext: { (flow, step) in
print ("did navigate to flow=\(flow) and step=\(step)")
}).disposed
将会生成如下 log:
1
2
3
4
5
6
7
8
9
10
11
12
13
did navigate flow=RxFlowDemo.MainFlow step=apiKeyIsComplete
did navigate flow=RxFlowDemo.WishlistFlow step=movieList
did navigate flow=RxFlowDemo.WatchedFlow step=movieList
did navigate flow=RxFlowDemo.WishlistFlow step=moviePicked(23452)
did navigate flow=RxFlowDemo.WishlistFlow step=castPicked(2)
did navigate flow=RxFlowDemo.WatchedFlow step=moviePicked(55423)
did navigate flow=RxFlowDemo.WatchedFlow step=castPicked(5)
did navigate flow=RxFlowDemo.WishlistFlow step=settings
did navigate flow=RxFlowDemo.SettingsFlow step=settings
did navigate flow=RxFlowDemo.SettingsFlow step=apiKey
did navigate flow=RxFlowDemo.SettingsFlow step=about
did navigate flow=RxFlowDemo.SettingsFlow step=apiKey
did navigate flow=RxFlowDemo.SettingsFlow step=settingsDone

日志对于分析和debug 程序很有帮助