Flutter 用 get_it + BLoC:拆出 UI 层 / Logic 层 / Bloc 层的完整架构实践

前言

下面是一篇围绕 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.dart
      • brand_model_event.dart
      • brand_model_state.dart
    • logic/
      • brand_model_logic.dart
    • data/
      • brand_model_repository.dart
      • brand_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,根据 isLoadinggroupedBrandserror 渲染;
  • 用户操作转为事件(如 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;
  • 等这一个模块跑顺了,再推广到其他模块。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容