Flutter 架构里总被混淆的 IoC、DI、Service Locator,到底是什么关系?

很多 Flutter 开发者在聊架构时,经常会把 IoC、DI、Service Locator 混着说。

尤其在 GetIt、GetX、Provider、Riverpod 这些方案放在一起讨论时,经常会出现几种典型表述:

  • GetX 也支持 DI
  • GetIt 是依赖注入框架
  • Riverpod 比 Service Locator 更工程化
  • IoC 不就是 DI 吗

这些话都不算完全错,但如果不把概念边界拆清楚,项目一大,讨论就会越来越乱。

这篇文章我想用 Flutter 工程视角,把这三个概念讲清楚:

  • IoC 到底是什么
  • DI 和 Service Locator 到底有什么区别
  • 为什么很多中大型项目更偏向显式依赖,而不是全局查找
  • 在 Flutter 里,GetIt、GetX、Riverpod 分别更接近哪种思路

一、先说结论:三者不是并列关系

如果只记一句话,我建议记这个:

  • IoC 是思想
  • DI 是实现 IoC 的一种方式
  • Service Locator 也是实现 IoC 的一种方式

也就是说,IoC 是上层抽象,DI 和 Service Locator 是两种常见落地手段。

所以它们的关系不是:

  • IoC vs DI vs Service Locator

而更像:

  • IoC
  • ├── DI
  • └── Service Locator

二、IoC:不是某个技术,而是一种控制权转移

IoC 全称是 Inversion of Control,中文一般叫 控制反转。

这个词第一次听起来会有点玄,其实它的核心意思很朴素:

不要让对象自己决定依赖怎么创建、流程怎么装配,而是把这部分控制权交给外部。

看一个最常见的例子。

1. 没有 IoC 的写法

dart 复制代码
class UserController {
  final api = ApiService();
  final logger = Logger();
}

这里 UserController 自己创建了 ApiService 和 Logger。

这意味着:

  • 它强依赖具体实现
  • 很难替换成 mock
  • 依赖关系写死在类内部
  • 测试和扩展都不优雅

2. 引入 IoC 思想后的写法

dart 复制代码
class UserController {
  final ApiService api;
  final Logger logger;

  UserController({
    required this.api,
    required this.logger,
  });
}

这时 UserController 不再自己创建依赖,而是由外部把依赖交给它。

这就是控制权的变化:

  • 过去:类自己控制依赖创建
  • 现在:外部系统控制依赖装配

这就是 IoC 的核心。

所以你可以把 IoC 理解为:

对象只关心"我要什么",不关心"这些东西怎么被创建出来"。


三、DI:依赖注入,是 IoC 最常见的工程实现

DI 全称 Dependency Injection,中文叫 依赖注入。

它是 IoC 最常见、也最推荐的一种落地方式。

核心思想是:

一个对象需要什么依赖,不要自己去创建,而是由外部传进来。

1. 构造函数注入

dart 复制代码
class UserController {
  final ApiService api;

  UserController(this.api);
}

外部装配:

dart 复制代码
final controller = UserController(ApiService());

这是最典型的 DI。

2. 为什么 DI 更受推崇?

因为它有几个很强的工程特性:

  • 依赖关系显式
  • 类的构造函数就是依赖清单
  • 更利于单元测试
  • 更容易替换实现
  • 更符合模块解耦原则

比如测试时,你可以直接传一个假实现:

dart 复制代码
final controller = UserController(FakeApiService());

而不需要修改 UserController 内部逻辑。

3. DI 的关键不在"有没有容器"

很多人把 DI 和"注册容器"绑定在一起,其实不完全对。

DI 的重点不是有没有一个神奇容器,而是:

  • 依赖是否从外部进入
  • 依赖关系是否显式
  • 对象是否自己创建依赖

只要依赖是外部传入,本质上就属于 DI。


四、Service Locator:不是外部传给我,而是我自己去拿

Service Locator 中文一般叫 服务定位器。

它和 DI 最大的差异在于:

  • DI:外部把依赖传给我
  • Service Locator:我自己去容器里找依赖

来看代码。

1. Service Locator 写法

dart 复制代码
class UserController {
  final ApiService api = locator<ApiService>();
}

或者在 Flutter 里更常见一点:

dart 复制代码
class UserController {
  final ApiService api = GetIt.I<ApiService>();
}

或者:

dart 复制代码
class UserController {
  final ApiService api = Get.find<ApiService>();
}

这些都属于 Service Locator 风格。

2. 它有什么优点?

优点也很明显:

  • 用起来简单
  • 少写构造参数
  • 接入速度快
  • 小项目开发效率高

3. 它的问题在哪里?

问题在于:依赖变成了"隐式的"。

看构造函数时你可能会发现:

dart 复制代码
class UserController {
  final ApiService api = Get.find<ApiService>();
  final Logger logger = Get.find<Logger>();
}

这个类表面上没有任何构造参数,但实际上偷偷依赖了两个外部服务。

这会带来几个问题:

  • 类的依赖边界不清晰
  • 阅读代码时不容易看出依赖关系
  • 测试需要先准备全局容器
  • 项目大了之后容易形成隐式耦合

所以很多资深开发者不是反对 Service Locator,而是反对:

在大型业务代码里到处无约束地使用 Service Locator。


五、DI 和 Service Locator 的真正分水岭:依赖是否显式

如果让我用一句话概括两者区别,我会说:

DI 把依赖写在明面上,Service Locator 把依赖藏在实现里。

DI 的特点

dart 复制代码
class UserController {
  final ApiService api;
  final Logger logger;

  UserController({
    required this.api,
    required this.logger,
  });
}

优势:

  • 一眼看出依赖项
  • 可测试性强
  • 重构更安全
  • 更适合大型项目治理

Service Locator 的特点

dart 复制代码
class UserController {
  final ApiService api = GetIt.I<ApiService>();
  final Logger logger = GetIt.I<Logger>();
}

优势:

  • 少写样板
  • 拿依赖方便
  • 上手门槛低

代价:

  • 依赖关系被隐藏
  • 全局容器容易扩散
  • 运行时问题多于编译期问题

所以从工程治理角度,很多团队会更偏向:

  • 核心业务层:显式 DI
  • 基础设施层:谨慎使用 Service Locator

六、回到 Flutter:GetIt、GetX、Riverpod 分别属于哪一类?

这部分是 Flutter 开发者最容易产生误解的地方。


1. GetIt:典型的 Service Locator

GetIt 本质上就是一个非常经典的 Service Locator。

dart 复制代码
final getIt = GetIt.instance;

void setup() {
  getIt.registerLazySingleton<ApiService>(() => ApiService());
}

class UserController {
  final api = getIt<ApiService>();
}

它的特点是:

  • 轻量
  • 简单
  • 不依赖 Widget 树
  • 很适合注册全局服务

如果你的诉求只是:

  • 管理全局 service
  • 管理 repository
  • 管理 config

那 GetIt 很顺手。

但如果业务层到处 getIt<T>(),项目就会慢慢演变成"谁都能从全局拿任何东西",后期维护压力会明显增加。


2. GetX:Service Locator + 状态 + 路由 一体化

GetX 不是单纯的 DI 工具,它是:

  • 状态管理
  • 路由管理
  • 依赖查找

三件事一起做。

dart 复制代码
Get.put(ApiService());

class UserController extends GetxController {
  final api = Get.find<ApiService>();
}

从依赖获取方式看,GetX 更接近 Service Locator,而不是纯粹的显式 DI。

它最大的优势是:

  • 开发快
  • 样板少
  • 心智负担低
  • 很适合快速迭代

但它最大的风险也很明显:

  • Controller 容易变胖
  • 状态、路由、依赖容易混在一起
  • 依赖边界容易失控

所以 GetX 不是不能做大项目,而是更依赖团队的自律和分层规范。


3. Riverpod:更接近"显式依赖图"而不是全局查找

Riverpod 的思路和 GetIt、GetX 不太一样。

它不是鼓励你"到处 find",而是通过 provider 之间的组合,把依赖关系写出来。

dart 复制代码
final apiServiceProvider = Provider<ApiService>((ref) {
  return ApiService();
});

final userRepositoryProvider = Provider<UserRepository>((ref) {
  final api = ref.watch(apiServiceProvider);
  return UserRepository(api);
});

这套方式的特点是:

  • 依赖关系显式
  • 不依赖 BuildContext
  • 天然支持 override
  • 生命周期表达更清晰
  • 更适合中大型项目治理

如果从设计风格上看,Riverpod 更像:

用声明式依赖图去做依赖注入和状态管理。

这也是为什么很多人会说:

  • GetIt/GetX 更像"去容器里拿东西"
  • Riverpod 更像"把依赖关系写成图"

七、为什么大型项目更偏向显式依赖?

这件事的本质,不是"Service Locator 不高级",而是:

项目一大,最大的成本不是写代码,而是理解依赖关系。

一个项目发展到后期,最怕三件事:

  • 隐式依赖
  • 全局耦合
  • 测试困难

1. 隐式依赖会让重构变慢

如果类的依赖都藏在实现内部,重构时你很难第一时间知道:

  • 改这个类会影响谁
  • 它到底依赖了几个服务
  • 哪些测试需要一起改

2. 全局耦合会让边界模糊

一旦全局容器成为万能仓库,大家就会习惯性地:

  • 缺什么就拿什么
  • 不想传参就全局查
  • 最后模块边界越来越弱

3. 测试会越来越重

如果一个对象依赖全局容器,你在测试它之前往往得先:

  • 注册 mock
  • 清理旧状态
  • 保证运行顺序正确

这会让测试从"验证业务"变成"先搭环境"。

所以从长期维护角度看,显式依赖通常更稳。


八、我的工程建议:不要把工具用成宗教

在实际项目里,我并不主张绝对化地说:

  • DI 一定比 Service Locator 好
  • Service Locator 一定不能用

我更倾向这样分层:

适合 Service Locator 的地方

  • 基础设施层
  • 全局配置
  • Logger
  • Analytics
  • App 级单例服务

更适合显式 DI 的地方

  • 业务层
  • UseCase
  • Repository
  • Controller / ViewModel
  • 可测试的核心逻辑模块

一句话就是:

容器可以存在,但不要让业务代码对"全局拿依赖"形成路径依赖。

最后总结

如果只用三句话总结,我会这样收口:

  • IoC 解决的是"控制权归谁"
  • DI 解决的是"依赖怎么显式传入"
  • Service Locator 解决的是"依赖怎么统一查找"

再工程化一点说:

  • 小项目里,Service Locator 往往很高效
  • 大项目里,显式 DI 往往更可维护
  • Flutter 选型时,重点不是 API 多方便,而是依赖关系是否清晰、是否可测试、是否有边界

工具本身没有绝对优劣,真正决定项目上限的,永远是:

  • 依赖是否透明
  • 模块边界是否清晰
  • 团队是否有一致的约束

如果你也在 Flutter 项目里纠结 GetIt、GetX、Riverpod 怎么选,不妨先别问"哪个最流行",先问自己一句:

我的项目现在最大的复杂度,到底是来自功能本身,还是来自依赖关系失控?

很多时候,答案会直接决定你的架构选型。

复制代码
相关推荐
事圆则缓6 小时前
Flutter 与原生交互的最后一层:RTC 视频帧怎么走
flutter·音视频·实时音视频
2601_949847756 小时前
jose 跨端三方库仓颉适配全流程
前端·javascript·flutter
2601_949970699 小时前
msgpackr 跨端三方库仓颉适配全流程
前端·javascript·flutter
2601_9499706921 小时前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
传奇开心果编程1 天前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘1 天前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra421 天前
不想开会员,我写了一个记账 App
flutter
2601_949575861 天前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间1 天前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios