/app:di — 부품 연결 담당#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /app:di |
| 별칭 | /di:create, /wiring:setup |
| 모델 | sonnet |
| 사용 도구 | Read, Edit, Write, Glob, Grep |
| 연계 스킬 | bloc |
한마디로#
앱을 이루는 여러 부품(화면·기능·데이터)을 서로 올바르게 연결해 주는 "전기 배선" 담당입니다. 레고 블록을 따로따로 만들어 두면 소용없듯, 이 명령이 블록들을 제 위치에 끼워 맞춰 앱이 실제로 동작하게 해줍니다.
누가·언제 쓰나요#
- 새 기능을 만든 뒤, 그 기능이 앱 전체와 제대로 이어지도록 "연결 작업"이 필요한 개발자
/app:di명령을 직접 실행할 때- 기능 전체 자동 생성(
/cc-flutter:feature:create)이 진행되는 도중, 부품을 합치는 단계에서 자동으로 호출될 때
무엇을 해주나요#
연결 방식을 옛날 방식(getIt/injectable)에서 깔끔한 새 방식으로 통일해 줍니다. 구체적으로는:
-
앱 전체에서 공용으로 쓰는 기능(예: 로그인
AuthBloc, 알림NotificationBloc)을 앱 시작 지점(bootstrap.dart)에서 만들어 연결 - 개별 화면이 자기 기능을 직접 만들어 쓰도록 배선
- 서로 의존하는 부품 간의 "꼬임(순환 의존)" 문제를 풀어줌
- 이 모든 규칙을 지켰는지 점검할 체크리스트 제공
어떻게 쓰나요#
# 기본 실행
/app:di
# 같은 기능을 부르는 다른 이름(별칭)도 사용 가능
/di:create
/wiring:setup
전달할 수 있는 값:
feature_name(필수) — 연결할 기능 모듈 이름 (snake_case 형식)-
module_type(선택) —app/console/common중 선택 (기본값app) is_global(선택) — 앱 전체 공용 기능인지 여부 (기본값false)
안에서 무슨 일이 벌어지나요#
-
금지된 옛날 방식 제거 —
getIt,@injectable같은 더 이상 쓰지 않는 연결 코드를 쓰지 않도록 막습니다. - 화면별 기능 연결 — 각 화면이 필요한 기능을 직접 생성해 끼워 넣습니다.
- 공용 기능 연결 — 앱 시작 지점에서 로그인·알림 같은 공용 부품을 만들고 앱 전체에 공유합니다.
- 꼬임 풀기 — 서로 맞물린 부품(알림 ↔ 로그인)의 순환 의존을 분리해 안전하게 초기화합니다.
- 점검 — 모든 규칙(금지 패턴 미사용, 올바른 생성 방식 등)을 체크리스트로 확인합니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Role#
Sets up Pure DI-based dependency wiring for bloc_signals containers.
- Global container creation (bootstrap.dart)
BlocSignalProvider.valuePattern (App)- Page container direct creation (
BlocSignalProvider) - UseCase Optional Constructor Injection
- Router initialization (initializeRouter)
Ownership contract (bloc_signals)#
| Form | Creates | Closes on dispose | Use for |
|---|---|---|---|
BlocSignalProvider(create: ...) | Yes | Yes | Page-scoped containers |
BlocSignalProvider.value(value: ...) | No | No | Global containers owned by bootstrap.dart |
This is why global containers use .value: bootstrap.dart owns their lifetime, so the provider must
not close them. Closing from both owners is an ownership bug even though close() is idempotent.
(bloc_signals_lint: avoid_providing_existing_instance_with_create, avoid_manual_close_on_provided_bloc)
Activation Conditions#
/app:diActivated when command is invoked/cc-flutter:feature:createorchestration integration phase
Parameters#
| Parameter | Required | Description |
|---|---|---|
feature_name | ✅ | Feature module name (snake_case) |
module_type | ❌ | app, console, common (default: app) |
is_global | ❌ | Global BLoC Whether (default: false) |
Removed Patterns (Prohibited)#
// ❌ All removed
getIt<T>()
getIt.registerLazySingleton<T>(...)
getIt.registerFactory<T>(...)
@injectable / @lazySingleton / @singleton
@microPackageInit
MicroPackageModule / PackageModule
injector.dart / injector.module.dart
di/ 폴더
ExternalModule(...)
export 'src/di/injector.dart'
Core Patterns#
1. Page BLoC (Most common)#
class {Feature}Page extends StatelessWidget {
const {Feature}Page({super.key});
@override
Widget build(BuildContext context) {
return BlocSignalProvider(
create: (_) => {Feature}Bloc()
..add(const {Feature}Event.loadRequested()),
child: const {Feature}View(),
);
}
}
2. Global BLoC (Created in bootstrap.dart)#
Authoritative DI paths (verify against the kobic monorepo):
ServiceLocator(get_it 대체) →package/core/lib/src/app/service_locator.dart- App-level wiring /
ServiceLocator.instance.registerSingleton(...)→app/kobic/lib/bootstrap.dartandapp/kobic_console/lib/bootstrap.dart- ❌ No
package/core/lib/src/di/folder —.../di/app_service_locator.dartand.../di/console_service_locator.dartdo not exist.
// app/kobic/lib/bootstrap.dart (console: app/kobic_console/lib/bootstrap.dart)
final notificationBloc = NotificationBloc();
final authBloc = AuthBloc(notificationBloc: notificationBloc);
notificationBloc.initAuthListener(authBloc);
runApp(App(blocs: GlobalBlocProviders(
authBloc: authBloc,
notificationBloc: notificationBloc,
)));
3. App (BlocSignalProvider.value)#
class App extends StatelessWidget {
const App({required this.blocs, super.key});
final GlobalBlocProviders blocs;
@override
Widget build(BuildContext context) {
return MultiBlocSignalProvider(
providers: [
BlocSignalProvider<AuthBloc>.value(value: blocs.authBloc),
BlocSignalProvider<NotificationBloc>.value(value: blocs.notificationBloc),
],
child: const MaterialApp(...),
);
}
}
4. BLoC Generation (Optional Constructor Injection)#
class {Feature}Bloc extends BlocSignal<{Feature}Event, {Feature}State> {
{Feature}Bloc({
Get{Entity}UseCase? get{Entity}UseCase,
AuthBloc? authBloc, // cross-BLoC: nullable
}) : _get{Entity}UseCase = get{Entity}UseCase ?? const Get{Entity}UseCase(),
_authBloc = authBloc,
super(initialState: const {Feature}State());
final Get{Entity}UseCase _get{Entity}UseCase;
final AuthBloc? _authBloc;
}
5. UseCase (Direct creation)#
class Get{Entity}UseCase {
const Get{Entity}UseCase([I{Feature}Repository? repo])
: _repo = repo ?? const {Feature}Repository();
final I{Feature}Repository _repo;
Future<Either<Failure, {Entity}>> call(Get{Entity}Params params) {
return _repo.get{Entity}(params);
}
}
6. Accessing a container from a Widget#
// ✅ Use context.read in callbacks (no rebuild dependency)
context.read<AuthBloc>().add(const AuthEvent.signOut());
// ✅ Read a narrow slice inside build (callback receives the CONTAINER)
final isSignedIn = context.select<AuthBloc, bool>(
(bloc) => bloc.stateValue is Authenticated,
);
// ❌ getIt usage prohibited
// ❌ context.watch<T>().stateValue — watch does NOT subscribe to state,
// it only rebuilds when the provided instance changes. Use BlocSignalBuilder.
// ❌ emit()/add() inside build() (avoid_emit_in_build)
7. Router Initialization#
AppRouter.initializeRouter(authBloc);
Feature Module Barrel File#
// lib/{feature_name}.dart
library;
export 'src/data/data.dart';
export 'src/domain/domain.dart';
export 'src/presentation/presentation.dart';
export 'src/route/route.dart';
// No di/ export!
Circular Dependency Resolution#
// NotificationBloc <-> AuthBloc circular dependency resolution
final notificationBloc = NotificationBloc();
final authBloc = AuthBloc(notificationBloc: notificationBloc);
notificationBloc.initAuthListener(authBloc); // Separated initialization
Test Patterns#
class MockGetDataUseCase extends Mock implements GetDataUseCase {}
blocSignalTest<{Feature}Bloc, {Feature}State>(
'emits [loading, loaded] when load succeeds',
setUp: () {
when(() => mockGetData(any()))
.thenAnswer((_) async => right(testData));
},
build: () => {Feature}Bloc(get{Entity}UseCase: mockGetData),
act: (bloc) => bloc.add(const {Feature}Event.loadRequested()),
expect: () => [
const {Feature}State.loading(),
{Feature}State.loaded(data: testData),
],
);
Checklist#
- Do not create di/ folder
- Do not use getIt, @injectable
- UseCase:
const UseCase([IRepo? repo]) : _repo = repo ?? ConcreteRepo() - BLoC: Optional Constructor Injection
- Page:
BlocSignalProvider(create: (_) => Bloc())— provider owns and closes it - Global container: Create in bootstrap.dart
- App:
BlocSignalProvider.valuePattern — does not close (bootstrap owns it) - Widget:
context.read<Bloc>()in callbacks;BlocSignalBuilder/selectfor state - Container:
super(initialState: ...)named arg,stateValuefor reads - Handler emit param typed
void Function(S)(noEmitter<S>type exists) - Router:
AppRouter.initializeRouter(authBloc) - Test: Constructor Direct injection