在前面的分享中,我们已经建立了对“显式依赖注入”的深刻认同。现在,我们面临最后一个,也是最关键的架构决策:如何组织我们应用的导航逻辑?我们是应该改良现有的Router模式,还是全面转向Coordinator模式?
我将首先清晰地辨析Coordinator与Router的本质区别,以阐明我们做出选择的理由。然后,我将结合MVVM和现代并发框架,为大家呈现一套完整的Coordinator模式最佳实践。
第一部分:正本清源 - Coordinator vs. Router
在很多讨论中,Coordinator和Router经常被混用,但它们在设计哲学和实现细节上有着天壤之别。
| 特性 | Router (URL-based 或 Protocol-based) | Coordinator |
|---|---|---|
| 核心隐喻 | 全局“电话总机”或“URL调度中心” | 专职“旅行团导游” |
| 核心职责 | 响应一个标识符(URL或协议),触发一个未知的跳转 | 管理一个完整、具象的业务流程(Use Case) |
| 调用方式 | Router.open("app://profile?id=123") | profileCoordinator.start(userId: "123") |
| 参数传递 | 通常是弱类型(字符串),难以传递复杂对象 | 强类型,可以通过构造函数或方法传递任何对象 |
| 依赖管理 | 隐式(黑盒):调用方不知道目标页面需要什么 | 显式(白盒):Coordinator负责为页面注入其所有依赖 |
| 导航控制 | 全局或分散,导航逻辑(push/present)与业务流分离 | 内聚,由Coordinator自身完全控制其流程内的导航 |
| 生命周期 | 通常是全局单例 | 有自己的生命周期,可以随业务流创建和销毁 |
结论:为何必须选择Coordinator?
Router模式的根本问题在于它是一个服务定位器(Service Locator)。它鼓励了“主动查询”的行为,隐藏了依赖关系,牺牲了类型安全和可测试性。任何试图改良Router的努力,都只是在为一个有缺陷的地基做表面装修。
而Coordinator模式是一次架构思想的升维。它天生就与依赖注入思想完美契合:
- 它自身通过DI被创建和配置。
- 它作为DI的执行者,为它所管理的MVVM栈注入依赖。
- 它将导航逻辑(“How”)与业务意图(“What”)清晰地分离开来。
因此,我们的决策是明确且坚定的:在应用内部导航中,全面采用Coordinator模式,彻底摒弃基于服务定位器的Router模式。 仅在需要响应外部事件(推送、H5)时,可以保留一个极轻量的URLDispatcher,其唯一作用是解析URL并启动一个根Coordinator。
第二部分:最佳实践 - Coordinator + MVVM + 现代并发
现在,让我们进入实战环节。以下是一套完整的、值得在所有新项目中推行的最佳实践。
- DI容器 (Swinject): 超级工厂。只在组合根中配置,负责创建一切。
- Coordinator: 交通指挥。负责业务流、依赖组装和导航。
- MVVM (View - ViewModel): 场景呈现者。ViewModel处理逻辑和状态,View负责渲染。
2. 核心原则:清晰的通信边界
- Coordinator -> ViewModel: 通过构造函数注入依赖(如Service)。
- ViewModel -> Coordinator: 绝对不能反向持有Coordinator的引用! ViewModel通过响应式信号(Combine/AsyncStream)或闭包向外发送导航“意图”。
- ViewModel <-> View: 通过
@StateObject/@ObservedObject和@Published属性进行双向绑定。
3. 完整示例:一个支持回调的“商品选择”流程
想象一个场景:在创建订单页面,需要弹出一个商品选择页面,选择完商品后,需要将商品ID回调给创建订单页面。
步骤1:定义Coordinator协议和父子关系
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
// 方便地管理子Coordinator
extension Coordinator {
func addChild(_ childCoordinator: Coordinator) {
childCoordinators.append(childCoordinator)
}
func removeChild(_ childCoordinator: Coordinator) {
childCoordinators = childCoordinators.filter { $0 !== childCoordinator }
}
}
步骤2:实现ProductSelectionCoordinator
import Combine
class ProductSelectionCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
private let resolver: Resolver
// 使用Combine的Subject来传递回调结果,类型安全
let selectionResult = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
init(resolver: Resolver, navigationController: UINavigationController) {
self.resolver = resolver
self.navigationController = navigationController
}
func start() {
let viewModel = resolver.resolve(ProductSelectionViewModel.self)!
let viewController = ProductSelectionViewController(viewModel: viewModel)
// 监听ViewModel的输出
viewModel.didSelectProduct
.sink { [weak self] productId in
// 1. 将结果通过自己的Subject发射出去
self?.selectionResult.send(productId)
}
.store(in: &cancellables)
viewModel.didCancel
.sink { [weak self] in
// 2. 空转结果,表示取消
self?.selectionResult.send(completion: .finished)
}
.store(in: &cancellables)
// 通常以模态形式弹出
navigationController.present(UINavigationController(rootViewController: viewController), animated: true)
}
}
步骤3:实现ProductSelectionViewModel
import Combine
class ProductSelectionViewModel: ObservableObject {
private let productService: ProductServicing
// 输出给Coordinator的信号
let didSelectProduct = PassthroughSubject<String, Never>()
let didCancel = PassthroughSubject<Void, Never>()
// 输出给View的状态
@Published var products: [Product] = []
init(productService: ProductServicing) {
self.productService = productService
}
// 供View调用的方法
func selectProduct(at index: Int) {
let productId = products[index].id
didSelectProduct.send(productId)
}
func cancelButtonTapped() {
didCancel.send()
}
@MainActor
func fetchProducts() async {
self.products = await productService.fetchAll()
}
}
步骤4:在父Coordinator (CreateOrderCoordinator) 中使用它
class CreateOrderCoordinator: Coordinator {
// ...
func showProductSelection() {
let selectionCoordinator = resolver.resolve(ProductSelectionCoordinator.self, argument: self.navigationController)!
addChild(selectionCoordinator)
selectionCoordinator.selectionResult
.sink(receiveCompletion: { [weak self] _ in
// 无论成功或取消,流程结束,都移除子Coordinator
self?.removeChild(selectionCoordinator)
self?.navigationController.dismiss(animated: true)
}, receiveValue: { [weak self] productId in
// 成功获取到商品ID,更新自己的ViewModel
self?.viewModel.update(with: productId)
})
.store(in: &cancellables)
selectionCoordinator.start()
}
// ...
}
4. 易错点与最终建议
- 生命周期管理是关键: 必须通过
addChild和removeChild来正确管理Coordinator的生命周期,否则将导致内存泄漏。 - 通信必须单向:
ViewModel绝不能知道Coordinator的存在。通信永远是ViewModel -> Coordinator的单向信号。 - DI容器的纯洁性:
ViewModel和View中绝对不能出现container.resolve的代码。依赖必须在初始化时注入。
总结:一套值得信赖的架构
通过将DI容器、Coordinator和MVVM三者有机结合,我们建立了一个分层清晰、职责单一、高度可测的黄金架构。它解决了传统MVC和Router模式的种种弊病,为我们构建复杂、可维护的大型应用提供了坚实的基础。
在第七篇中,我将会介绍一个 MVVMC 的实际使用示例,敬请期待。