前言
下面是一篇围绕 Flutter + get_it + BLoC 的博客,目标是解决一个现实问题:
随着项目中业务复杂,你的BLoC 越写越胖、越难维护**,我们如何通过增加一个 Logic 层,把 BLoC 变得「又瘦又清晰」。

image.png
一、问题从哪来:为什么 BLoC 会越来越臃肿?
很多项目里,我们会这样用 BLoC:
-
UI 层
-
BlocBuilder/BlocConsumer监听state; - 点击按钮时调用
context.read<MyBloc>().add(MyEvent())。
-
-
BLoC 层
- 接收各种事件(Event);
- 在
on<Event>里写大量业务逻辑:参数校验、接口调用、异常处理、数据转换、状态切换…… - 最后
emit新状态(State)。
一开始业务简单时没什么问题,但当需求滚雪球式增加时,BLoC 的痛点会集中爆发:
- 代码行数爆炸:一个 BLoC 几百到上千行很常见;
- 职责混乱:既要管「事件 → 状态」这种状态机逻辑,又要负责一堆业务流程;
- 复用困难:某段业务逻辑想在另一个地方用,只能复制粘贴或硬抽到工具函数;
- 测试成本高:真正想测的「业务逻辑」被裹挟在事件和状态转换中,拆不干净。
底层原因(知其所以然):
- 从设计上看,BLoC 更适合作为状态管理与事件调度器;
- 但在实战中,我们往往把「业务流程」也塞进了 BLoC;
- 导致一个类同时负责:
- 状态机(event → state);
- 业务用例(例如「登录」「下单」「车型选择」等的复杂流程)。
- 这违背了单一职责原则,复杂度自然就会「在一个类里爆炸」。
二、整体思路:在 UI 和 Repository 中间多加一个 Logic 层
我们希望的目标是:
-
UI 层(View)
- 只负责展示和交互(收集用户输入、响应用户操作);
- 不做复杂业务逻辑,不做异常兜底。
-
Bloc 层
- 只做:接收事件 → 调用对应业务逻辑 → 更新状态;
- 尽量不直接写复杂业务规则。
-
Logic 层
- 专门负责「某一块业务场景」的完整逻辑,比如:
- 登录流程;
- 品牌/车型选择;
- 订单创建与校验等;
- 内部可以调用 Repository、Service;
- 集中承载业务复杂度。
- 专门负责「某一块业务场景」的完整逻辑,比如:
-
Repository / Service 层
- 负责数据访问:网络请求、本地数据库、缓存等;
- 专注「数据从哪来」,不关心具体业务场景。
这样形成一条清晰的调用链:
UI → Bloc → Logic → Repository / Service → Logic → Bloc → UI
关键点:
- BLoC 不再承担「业务细节」,只负责流程串接和状态更新;
- 业务复杂度集中在 Logic 层,后期维护只要优先看 Logic 即可。
三、get_it 的角色:让依赖关系清晰又好管理
get_it 是一个简单的依赖注入(DI)/ Service Locator 工具,它解决的问题是:
「谁需要谁」这件事,应该被统一配置,而不是散落在各个文件里。
在我们这套架构中的依赖关系大致是:
- Logic 依赖 Repository;
- Bloc 依赖 Logic;
- UI 依赖 Bloc(通过
BlocProvider)。
不用 get_it 的话,我们可能在 UI 里写类似:
final api = BrandModelApi();
final repo = BrandModelRepositoryImpl(api);
final logic = BrandModelLogic(repository: repo);
final bloc = BrandModelBloc(logic: logic);
这样虽然能跑,但一旦项目增大,会有几个问题:
- 对象创建分散在各处,难以统一管理;
- 想在测试环境替换成 mock 版本会很痛苦;
- 想看整个系统「谁依赖谁」,只能满项目搜。
用 get_it 之后,我们可以统一在一个 injector.dart 里:
- 注册所有 Repository、Logic、Bloc;
- UI 中只需要
getIt<MyBloc>(),不关心如何构造。
这样有两个好处:
- 依赖拓扑一目了然:集中在一个文件里;
- 更容易做测试和切换实现:比如针对接口、缓存做 A/B 实验,用不同实现注册即可。
四、目录结构建议:按功能(feature)组织,而不是按技术
以「车品牌/车系选择」为例,可以这么组织:
-
lib/features/brand_model/-
ui/brand_model_select_page.dart
-
bloc/brand_model_bloc.dartbrand_model_event.dartbrand_model_state.dart
-
logic/brand_model_logic.dart
-
data/brand_model_repository.dartbrand_model_api.dart
-
-
lib/core/di/injector.dart
要点:
- 以「品牌/车型选择」这个业务为中心,所有相关 UI / Bloc / Logic / Data 都聚拢在一个 feature 目录下;
- Logic 就是这个 feature 的「业务中枢」;
- 这样当你接到一个需求「品牌选择的逻辑要改成 xxx」,你可以直接进这个模块,而不用跨越多个「大文件夹」。
五、从底到顶走一遍:Data → Logic → Bloc → UI
为了更贴近你的实际场景,我们用「品牌列表分组展示」做示例。
5.1 Data 层:只关心拿数据
class BrandModelApi {
Future<List<CarBrand>> fetchBrands() async {
// 模拟网络请求
await Future.delayed(const Duration(milliseconds: 300));
return [
CarBrand(id: 1, name: '奥迪', firstLetter: 'A'),
CarBrand(id: 2, name: '宝马', firstLetter: 'B'),
// ...
];
}
Future<List<CarModel>> fetchModelsByBrand(int brandId) async {
await Future.delayed(const Duration(milliseconds: 300));
return [
CarModel(id: 101, brandId: brandId, name: '示例车型'),
// ...
];
}
}
class CarBrand {
final int id;
final String name;
final String firstLetter;
CarBrand({required this.id, required this.name, required this.firstLetter});
}
class CarModel {
final int id;
final int brandId;
final String name;
CarModel({required this.id, required this.brandId, required this.name});
}
abstract class BrandModelRepository {
Future<List<CarBrand>> getBrands();
Future<List<CarModel>> getModels(int brandId);
}
class BrandModelRepositoryImpl implements BrandModelRepository {
final BrandModelApi api;
BrandModelRepositoryImpl(this.api);
@override
Future<List<CarBrand>> getBrands() => api.fetchBrands();
@override
Future<List<CarModel>> getModels(int brandId) =>
api.fetchModelsByBrand(brandId);
}
这里刻意保持「无业务」:
- 不做搜索过滤;
- 不做分组;
- 不做文案格式化。
这些都留给 Logic 层来做。
5.2 Logic 层:负责业务规则与数据加工
例如品牌列表需要:
- 按首字母分组;
- 支持关键词搜索;
- 统一错误提示。
class BrandListResult {
final Map<String, List<CarBrand>> groupedBrands;
final String? error;
BrandListResult({
required this.groupedBrands,
this.error,
});
bool get hasError => error != null;
}
class BrandModelLogic {
final BrandModelRepository repository;
BrandModelLogic({required this.repository});
/// 加载并分组品牌列表
Future<BrandListResult> loadGroupedBrands({String? keyword}) async {
try {
final brands = await repository.getBrands();
// 1. 关键词过滤(可选)
final filtered = (keyword == null || keyword.isEmpty)
? brands
: brands.where((b) => b.name.contains(keyword)).toList();
// 2. 按首字母分组
final Map<String, List<CarBrand>> grouped = {};
for (final brand in filtered) {
final key = brand.firstLetter.toUpperCase();
grouped.putIfAbsent(key, () => []).add(brand);
}
// 3. 每组内排序
for (final list in grouped.values) {
list.sort((a, b) => a.name.compareTo(b.name));
}
return BrandListResult(groupedBrands: grouped);
} catch (_) {
return BrandListResult(
groupedBrands: {},
error: '加载品牌列表失败,请稍后重试',
);
}
}
/// 加载某品牌下车型列表(这里也可以叠加缓存、策略等)
Future<List<CarModel>> loadModels(int brandId) async {
return repository.getModels(brandId);
}
}
这里体现 Logic 层的价值:
- 业务规则清晰集中:过滤、分组、错误文案都在这里;
- Logic 返回的是一个「业务结果对象」
BrandListResult,Bloc 不用关心细节; - 当未来品牌逻辑变化时,首选修改
BrandModelLogic而非 Bloc。
5.3 Bloc 层:事件转发 + 状态维护
事件:
abstract class BrandModelEvent {}
class LoadBrandList extends BrandModelEvent {
final String? keyword;
LoadBrandList({this.keyword});
}
class SelectBrand extends BrandModelEvent {
final CarBrand brand;
SelectBrand(this.brand);
}
状态:
class BrandModelState {
final bool isLoading;
final Map<String, List<CarBrand>> groupedBrands;
final CarBrand? selectedBrand;
final String? error;
const BrandModelState({
this.isLoading = false,
this.groupedBrands = const {},
this.selectedBrand,
this.error,
});
BrandModelState copyWith({
bool? isLoading,
Map<String, List<CarBrand>>? groupedBrands,
CarBrand? selectedBrand,
String? error,
}) {
return BrandModelState(
isLoading: isLoading ?? this.isLoading,
groupedBrands: groupedBrands ?? this.groupedBrands,
selectedBrand: selectedBrand ?? this.selectedBrand,
error: error,
);
}
}
Bloc 实现:
class BrandModelBloc extends Bloc<BrandModelEvent, BrandModelState> {
final BrandModelLogic logic;
BrandModelBloc({required this.logic}) : super(const BrandModelState()) {
on<LoadBrandList>(_onLoadBrandList);
on<SelectBrand>(_onSelectBrand);
}
Future<void> _onLoadBrandList(
LoadBrandList event,
Emitter<BrandModelState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null));
final result = await logic.loadGroupedBrands(keyword: event.keyword);
if (result.hasError) {
emit(
state.copyWith(
isLoading: false,
groupedBrands: {},
error: result.error,
),
);
} else {
emit(
state.copyWith(
isLoading: false,
groupedBrands: result.groupedBrands,
error: null,
),
);
}
}
void _onSelectBrand(
SelectBrand event,
Emitter<BrandModelState> emit,
) {
emit(state.copyWith(selectedBrand: event.brand));
}
}
可以看到:
- BLoC 中没有复杂的业务逻辑,只负责:
- 调用
logic.loadGroupedBrands; - 根据
BrandListResult更新状态;
- 调用
- 状态结构清晰,UI 知道应该展示什么。
5.4 get_it 注入:集中管理依赖
injector.dart:
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
Future<void> setupInjector() async {
// Data
getIt.registerLazySingleton<BrandModelApi>(() => BrandModelApi());
getIt.registerLazySingleton<BrandModelRepository>(
() => BrandModelRepositoryImpl(getIt<BrandModelApi>()),
);
// Logic
getIt.registerFactory<BrandModelLogic>(
() => BrandModelLogic(repository: getIt<BrandModelRepository>()),
);
// Bloc
getIt.registerFactory<BrandModelBloc>(
() => BrandModelBloc(logic: getIt<BrandModelLogic>()),
);
}
原则:
- 底层 Data 通常可以
LazySingleton; - 上层 Logic/Bloc 通常使用
Factory保证每次都有新实例; - 依赖链清晰:Bloc → Logic → Repository → Api。
5.5 UI 层:只和 Bloc 对话
class BrandModelSelectPage extends StatelessWidget {
const BrandModelSelectPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<BrandModelBloc>(
create: (_) => getIt<BrandModelBloc>()..add(LoadBrandList()),
child: const _BrandModelSelectView(),
);
}
}
class _BrandModelSelectView extends StatelessWidget {
const _BrandModelSelectView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择品牌与车型')),
body: BlocConsumer<BrandModelBloc, BrandModelState>(
listener: (context, state) {
if (state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!)),
);
}
},
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final groupKeys = state.groupedBrands.keys.toList()..sort();
return ListView.builder(
itemCount: groupKeys.length,
itemBuilder: (context, index) {
final letter = groupKeys[index];
final brands = state.groupedBrands[letter] ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Text(
letter,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
...brands.map(
(b) => ListTile(
title: Text(b.name),
onTap: () {
context.read<BrandModelBloc>().add(SelectBrand(b));
// 可以在这里跳转或返回结果
},
),
),
],
);
},
);
},
),
);
}
}
UI 做的事情非常单纯:
- 触发
LoadBrandList事件; - 监听
BrandModelState,根据isLoading、groupedBrands、error渲染; - 用户操作转为事件(如
SelectBrand)。
UI 不关心 Logic 和 Repository 的存在,这就是分层带来的解耦。
六、这样拆三层(UI / Logic / Bloc)带来的实质收益
1. 复杂度局部化
- 复杂业务逻辑集中在 Logic 层;
- BLoC 文件体积明显变小,只剩下事件与状态管理代码;
- 查问题时,「改业务 → 看 Logic」、「改状态 → 看 Bloc」,心智模型非常简单。
2. 测试友好
- Logic 层几乎可以作为「纯 Dart 逻辑」单独测试:
- 用 mock 的 Repository 即可;
- 不依赖 Widget 测试,不需要 BuildContext;
- Bloc 测试只关心:
- 给定一个 mock Logic;
- 发送事件是否能产生预期状态序列。
3. 更易演进
- 未来想换状态管理库(比如不用 BLoC),Logic 基本不需要修改;
- 未来想换网络库/缓存方案,只需要改 Data 层和 DI 注册;
- 架构「内核」稳定,周边技术栈可以平滑替换。
4. 团队协作顺畅
- 一部分人专注 UI + Bloc,另一部分人专注 Logic + Repository;
- 每个 Logic 类天然对应一块业务场景,便于业务对齐和交流;
- 代码评审时也能清晰区分「这是业务逻辑问题」还是「状态管理问题」。
七、总结与落地建议
-
架构划分:
- UI 层:展示 + 用户交互,不写复杂逻辑;
- Bloc 层:事件转发 + 状态管理;
- Logic 层:聚合复杂业务流程,是业务大脑;
- Repository / Service 层:提供原始数据通路。
- get_it:用来统一管理依赖关系,让 Bloc → Logic → Repository 的链条清晰可控。
如何在现有项目渐进式落地?
- 从一个最痛的「大 BLoC」开始,选其中一个业务功能先抽成 Logic 层;
- 保持对外行为不变(写单测覆盖),逐步把剩余逻辑迁移到 Logic;
- 等这一个模块跑顺了,再推广到其他模块。