很多 Flutter 开发者在聊架构时,经常会把 IoC、DI、Service Locator 混着说。
尤其在 GetIt、GetX、Provider、Riverpod 这些方案放在一起讨论时,经常会出现几种典型表述:
GetX 也支持 DIGetIt 是依赖注入框架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 怎么选,不妨先别问"哪个最流行",先问自己一句:
我的项目现在最大的复杂度,到底是来自功能本身,还是来自依赖关系失控?
很多时候,答案会直接决定你的架构选型。