前三篇我们已经为iOS原生开发的DI打下了坚实的基础。现在,让我们把目光投向更广阔的领域,看看在现代化的声明式UI范式下,DI的思想是如何演进和应用的。这对于我们组内同时拥有Swift和Flutter技术栈的同学来说,尤其有价值。
今天,我们要讨论一个非常前沿且重要的话-题:依赖注入在声明式UI框架中应该如何实践?
随着SwiftUI和Flutter的兴起,我们的UI构建方式已经从命令式(“去做这个,然后做那个”)转变为声明式(“UI应该是这个状态”)。这种转变不仅仅影响了视图层,它也深刻地改变了我们对状态管理和依赖注入的思考方式。
组内有很多Flutter的同事对Riverpod框架非常熟悉。这是一个绝佳的机会,我们可以通过对比Riverpod的设计哲学,来反思和探索在SwiftUI中进行依赖注入的最佳实践。
1. 求同存异,先看Flutter的Riverpod
对于不熟悉Riverpod的Swift同学,可以把它简单理解为一个“超级强大”的DI和状态管理框架。它的核心是 Provider。
一个Provider就是一个“提供者”,它可以向UI(在Flutter中是Widget)提供任何东西:
- 一个服务的实例(如
APIService) - 一个计算后的值(如从多个状态组合出的新值)
- 一个完整的ViewModel(在Flutter中通常叫
StateNotifier或ChangeNotifier)
Riverpod的核心特点:
- 与UI框架深度融合:
Provider的生命周期和作用域可以与Widget树紧密绑定。当一个Widget不再需要某个Provider时,Provider可以被自动销毁(autoDispose),非常高效地管理内存。 - 天生的响应式: 当一个
Provider所提供的数据发生变化时,所有“监听”了这个Provider的UI组件都会自动重建(刷新),以展示最新的状态。 - 编译时安全: 它摆脱了Flutter早期DI框架
Provider需要依赖BuildContext(上下文)的缺点,可以在任何地方安全地访问,且没有运行时风险。 - 不仅仅是DI,更是状态管理: 这是最关键的一点。Riverpod通过
Provider统一了“依赖注入”和“状态管理”这两个概念。你可以用同样的方式去“提供”一个无状态的APIService和一个有状态的CounterViewModel。
// Riverpod 示例
// 1. 提供一个服务
final apiServiceProvider = Provider((ref) => ApiService());
// 2. 提供一个ViewModel(StateNotifier)
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
// 可以在这里读取其他provider!实现了依赖注入
final apiService = ref.watch(apiServiceProvider);
return Counter(apiService); // Counter是StateNotifier的子类
});
// 3. 在UI中使用
class MyScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 读取ViewModel的状态
final count = ref.watch(counterProvider);
// 读取ViewModel本身以调用其方法
final counterNotifier = ref.read(counterProvider.notifier);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => counterNotifier.increment(),
),
);
}
}
2. 对比Swift世界的DI:核心差异
通过观察Riverpod,我们可以发现它与我们之前讨论的Swinject等传统DI框架的核心差异:
-
关注点不同:
- Swinject (传统DI): 更关注 “对象图的构建” (Object Graph Construction)。它的核心任务是在应用启动时或需要时,正确地创建和连接好所有对象。它对UI层是“无知”的。
- Riverpod: 更关注 “状态的提供与消费” (State Provision and Consumption)。它与UI层紧密耦合,其设计初衷就是为了服务于响应式的UI刷新。
-
生命周期/作用域不同:
- Swinject: 它的作用域(
.container,.graph)与Container实例和resolve调用相关,与UI组件的生命周期没有直接关系。 - Riverpod:
Provider的生命周期可以和Widget的生命周期完全同步,实现了更精细、自动化的管理。
- Swinject: 它的作用域(
3. SwiftUI中的DI最佳实践
那么,在SwiftUI中,我们应该如何借鉴这些思想呢?
a. 苹果的原生方案:@EnvironmentObject
SwiftUI提供了一个原生的DI机制:@EnvironmentObject。你可以把一个对象注入到视图环境中,任何子视图都可以从中读取。
// 在根视图注入
let userSettings = UserSettings()
ContentView().environmentObject(userSettings)
// 在子视图中接收
struct SettingsView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
Text("Username: \(settings.username)")
}
}
优点:
- 非常简单,苹果原生支持。
缺点:
- 类型不安全: 如果你忘记在父视图注入
environmentObject,App会在运行时直接崩溃。 - 全局污染: 它更像一个“全局变量”,适用于传递真正全局的、与UI显示相关的状态(如主题、用户设置),但如果用它来注入大量的服务和ViewModel,会使依赖关系变得混乱和隐式。
- 不适合注入服务: 它的设计初衷是传递
ObservableObject,用于驱动UI刷新,而不是注入无状态的服务。
结论:@EnvironmentObject可用,但要谨慎。它适合做简单的UI状态分发,而非完整的DI解决方案。
b. 主流方案:“DI容器 + ViewModel”模式
社区目前公认的最佳实践,是结合我们前几篇学到的知识,将传统DI容器(如Swinject)与SwiftUI的@StateObject / @ObservedObject结合起来。
这个模式的思路是:DI容器负责幕后的对象创建,SwiftUI负责前台的状态观测和UI刷新,二者各司其职。
实践步骤:
-
在组合根中设置DI容器: 和以前一样,在App的入口处(
@main的App结构体或SceneDelegate)创建和配置我们的Assembler和Container。// App.swift @main struct MyApp: App { let assembler: Assembler init() { assembler = Assembler([ NetworkAssembly(), ViewModelAssembly() ]) } var body: some Scene { WindowGroup { // 从容器中解析出根视图的ViewModel // 注意这里,我们只解析一次,然后交给SwiftUI管理 let rootViewModel = assembler.resolver.resolve(RootViewModel.self)! RootView(viewModel: rootViewModel) } } } -
为View注入其专属的ViewModel: View不应该直接接触DI容器。View的唯一依赖就是它的ViewModel。ViewModel通过构造函数注入它所需要的所有服务。
// ProductDetailView.swift struct ProductDetailView: View { // 使用@StateObject确保ViewModel的生命周期与View绑定 // viewModel由父视图(或Coordinator)创建并传入 @StateObject var viewModel: ProductDetailViewModel var body: some View { VStack { Text(viewModel.productName) if viewModel.isLoading { ProgressView() } Button("Add to cart") { viewModel.addToCart() } } .onAppear(perform: viewModel.fetchProduct) } } -
ViewModel从DI容器中创建,并注入依赖: 这一步发生在
ViewModelAssembly中,我们之前已经很熟悉了。// ViewModelAssembly.swift class ViewModelAssembly: Assembly { func assemble(container: Container) { // ViewModel必须是 .transient container.register(ProductDetailViewModel.self) { (r, productId: String) in ProductDetailViewModel( productId: productId, apiService: r.resolve(APIService.self)!, // 自动解析服务 cartService: r.resolve(CartService.self)! ) }.inObjectScope(.transient) } }
这个模式的巨大优势:
- 职责清晰: Swinject管创建,ViewModel管业务逻辑和状态,View管渲染。
- 强类型安全: 所有依赖都通过构造函数注入,编译时就能发现错误。
- 可测试性极高: 你可以轻松地为
ProductDetailViewModel创建mock的APIService和CartService来进行单元测试,完全不用依赖任何UI。 - 与SwiftUI和谐共存: 它没有破坏SwiftUI的声明式和响应式特性,而是为其提供了一个坚实的、可预测的数据和逻辑后端。
总结与思考
通过对比Riverpod,我们发现,虽然它在与UI的结合度上做得更“原生”,但其核心思想——将依赖(服务)和状态(ViewModel)统一通过某种机制(Provider)提供给UI——是相通的。
在SwiftUI中,我们虽然没有一个像Riverpod一样大一统的框架,但通过 “Swinject (DI容器) + @StateObject (状态管理)” 的组合,我们实现了一个逻辑上等价且非常强大的模式。
- Riverpod的
ref.watch(someProvider)≈ SwiftUI的@StateObject/@ObservedObject。 它们都负责监听状态变化并触发UI刷新。 - Riverpod的
Provider定义 ≈ Swinject的container.register。 它们都负责定义如何创建依赖和状态对象。
最终,我们为SwiftUI应用构建了一个清晰的分层架构:
View -> ViewModel (状态和业务逻辑) -> Services (无状态的原子能力)
而依赖注入,就是将这些层次优雅地“粘合”在一起的最佳胶水。
在下一篇中,我们将回到更广义的架构讨论,看看DI思想的其他实现方式,比如通过路由(Coordinator)模式,并给出最终的架构选型建议。
敬请期待!