Standard DI pattern for improving testability when using UseCase in BLoC.
Core Principles#
- Optional Constructor Injection: Inject UseCase as optional named parameter
-
Default Fallback: Provide production default with
?? const SomeUseCase() - Mock injection in tests: Direct mock passing via constructor
Basic Pattern (1-7 UseCase)#
@injectable
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({
GetDataUseCase? getDataUseCase,
UpdateDataUseCase? updateDataUseCase,
}) : _getDataUseCase = getDataUseCase ?? const GetDataUseCase(),
_updateDataUseCase = updateDataUseCase ?? const UpdateDataUseCase(),
super(const MyState()) {
on<_LoadData>(_onLoadData);
on<_UpdateData>(_onUpdateData);
}
final GetDataUseCase _getDataUseCase;
final UpdateDataUseCase _updateDataUseCase;
Future<void> _onLoadData(
_LoadData event,
Emitter<MyState> emit,
) async {
emit(state.copyWith(status: const MyStatus.loading()));
final result = await _getDataUseCase(GetDataParams(id: event.id));
if (isClosed) return;
emit(result.fold(
(failure) => state.copyWith(status: MyStatus.error(failure.toString())),
(data) => state.copyWith(status: const MyStatus.loaded(), data: data),
));
}
}
Rules#
| Item | Rule |
|---|---|
| Parameter type | GetDataUseCase? (nullable) |
| Default | ?? const GetDataUseCase() (const constructor) |
| Field | final GetDataUseCase _getDataUseCase (private, non-nullable) |
| Call | await _getDataUseCase(params) (.call() omitted) |
| After async | if (isClosed) return; required |
Bundle Pattern (8+ UseCase)#
BLoCs with 8 or more UseCases are grouped using a Bundle Class.
/// MyBloc에서 사용하는 UseCase 번들
class MyBlocUseCases {
const MyBlocUseCases({
this.getData = const GetDataUseCase(),
this.updateData = const UpdateDataUseCase(),
this.deleteData = const DeleteDataUseCase(),
this.searchData = const SearchDataUseCase(),
this.filterData = const FilterDataUseCase(),
this.sortData = const SortDataUseCase(),
this.exportData = const ExportDataUseCase(),
this.importData = const ImportDataUseCase(),
this.validateData = const ValidateDataUseCase(),
});
final GetDataUseCase getData;
final UpdateDataUseCase updateData;
final DeleteDataUseCase deleteData;
final SearchDataUseCase searchData;
final FilterDataUseCase filterData;
final SortDataUseCase sortData;
final ExportDataUseCase exportData;
final ImportDataUseCase importData;
final ValidateDataUseCase validateData;
}
@injectable
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({MyBlocUseCases? useCases})
: _useCases = useCases ?? const MyBlocUseCases(),
super(const MyState()) {
on<_LoadData>(_onLoadData);
}
final MyBlocUseCases _useCases;
Future<void> _onLoadData(
_LoadData event,
Emitter<MyState> emit,
) async {
final result = await _useCases.getData(GetDataParams(id: event.id));
if (isClosed) return;
// ...
}
}
Bundle File Location#
feature/{type}/{feature_name}/lib/src/presentation/bloc/{bloc_name}/
├── {bloc_name}_bloc.dart
├── {bloc_name}_event.dart
├── {bloc_name}_state.dart
└── {bloc_name}_use_cases.dart ← Bundle 클래스
Test Patterns#
Basic Pattern Test#
late MockGetDataUseCase mockGetDataUseCase;
late MockUpdateDataUseCase mockUpdateDataUseCase;
late MyBloc bloc;
setUp(() {
mockGetDataUseCase = MockGetDataUseCase();
mockUpdateDataUseCase = MockUpdateDataUseCase();
bloc = MyBloc(
getDataUseCase: mockGetDataUseCase,
updateDataUseCase: mockUpdateDataUseCase,
);
});
Bundle Pattern Test#
late MyBloc bloc;
setUp(() {
bloc = MyBloc(
useCases: MyBlocUseCases(
getData: MockGetDataUseCase(),
updateData: MockUpdateDataUseCase(),
// Mock only what's needed, rest use defaults
),
);
});
Using blocTest#
blocTest<MyBloc, MyState>(
'Transitions to loaded state on successful data load',
build: () {
when(() => mockGetDataUseCase(any()))
.thenAnswer((_) async => Right(testData));
return MyBloc(getDataUseCase: mockGetDataUseCase);
},
act: (bloc) => bloc.add(const MyEvent.loadData(id: 1)),
expect: () => [
isA<MyState>().having((s) => s.status, 'status', isA<MyStatusLoading>()),
isA<MyState>().having((s) => s.data, 'data', testData),
],
);
Before to After Migration#
Before (Inline UseCase Call)#
// ❌ WRONG: Inline const UseCase().call() usage
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc() : super(const MyState()) {
on<_LoadData>(_onLoadData);
}
Future<void> _onLoadData(_LoadData event, Emitter<MyState> emit) async {
final result = await const GetDataUseCase().call(
GetDataParams(id: event.id),
);
if (isClosed) return;
// ...
}
}
After (Optional Constructor Injection)#
// ✅ CORRECT: Optional Constructor Injection
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({
GetDataUseCase? getDataUseCase,
}) : _getDataUseCase = getDataUseCase ?? const GetDataUseCase(),
super(const MyState()) {
on<_LoadData>(_onLoadData);
}
final GetDataUseCase _getDataUseCase;
Future<void> _onLoadData(_LoadData event, Emitter<MyState> emit) async {
final result = await _getDataUseCase(GetDataParams(id: event.id));
if (isClosed) return;
// ...
}
}
Migration Checklist#
- Find all
const SomeUseCase().call(params)patterns - Add optional named parameter to constructor
- Set
?? const SomeUseCase()default in initializer list - Declare
finalfield - Change call to
_useCase(params)form in handlers - Remove explicit
.call()(Dart callable class) - Keep
@injectableannotation (GetIt compatible) - Verify existing tests pass
@injectable Compatibility#
Optional Constructor Injection is fully compatible with @injectable.
GetIt ignores optional parameters with defaults and only injects registered dependencies.
// When registering with GetIt: optional parameters use defaults
@injectable
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc({
GetDataUseCase? getDataUseCase, // GetIt이 주입하지 않음 → default값 사용
}) : _getDataUseCase = getDataUseCase ?? const GetDataUseCase(),
super(const MyState());
}
// Production: getIt<MyBloc>() -> uses default UseCase
// Test: MyBloc(getDataUseCase: mockUseCase) -> inject mock
Application Criteria#
| UseCase Count | Pattern | Note |
|---|---|---|
| 1-7 | Basic pattern (individual parameters) | Most BLoCs |
| 8+ | Bundle pattern (UseCases class) | Complex BLoCs |
Related Documents#
- feature/CLAUDE.md - BLoC Event/State Pattern, Clean Architecture
- dcm-bloc.md - BLoC DCM lint rules
- swr-pattern.md - Stream-based SWR UseCase Pattern
- CLAUDE.md - Complete project guide