LogoSkills

di-agent

BlocSignal Provider 구성과 의존성 배선 전문가입니다. 전역/페이지 컨테이너 생성과 bootstrap 설정에 사용합니다.

/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)

안에서 무슨 일이 벌어지나요#

  1. 금지된 옛날 방식 제거getIt, @injectable 같은 더 이상 쓰지 않는 연결 코드를 쓰지 않도록 막습니다.
  2. 화면별 기능 연결 — 각 화면이 필요한 기능을 직접 생성해 끼워 넣습니다.
  3. 공용 기능 연결 — 앱 시작 지점에서 로그인·알림 같은 공용 부품을 만들고 앱 전체에 공유합니다.
  4. 꼬임 풀기 — 서로 맞물린 부품(알림 ↔ 로그인)의 순환 의존을 분리해 안전하게 초기화합니다.
  5. 점검 — 모든 규칙(금지 패턴 미사용, 올바른 생성 방식 등)을 체크리스트로 확인합니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Role#

Sets up Pure DI-based dependency wiring for bloc_signals containers.

  • Global container creation (bootstrap.dart)
  • BlocSignalProvider.value Pattern (App)
  • Page container direct creation (BlocSignalProvider)
  • UseCase Optional Constructor Injection
  • Router initialization (initializeRouter)

Ownership contract (bloc_signals)#

FormCreatesCloses on disposeUse for
BlocSignalProvider(create: ...)YesYesPage-scoped containers
BlocSignalProvider.value(value: ...)NoNoGlobal 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:di Activated when command is invoked
  • /cc-flutter:feature:create orchestration integration phase

Parameters#

ParameterRequiredDescription
feature_nameFeature module name (snake_case)
module_typeapp, console, common (default: app)
is_globalGlobal 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.dart and app/kobic_console/lib/bootstrap.dart
  • ❌ No package/core/lib/src/di/ folder — .../di/app_service_locator.dart and .../di/console_service_locator.dart do 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.value Pattern — does not close (bootstrap owns it)
  • Widget: context.read<Bloc>() in callbacks; BlocSignalBuilder/select for state
  • Container: super(initialState: ...) named arg, stateValue for reads
  • Handler emit param typed void Function(S) (no Emitter<S> type exists)
  • Router: AppRouter.initializeRouter(authBloc)
  • Test: Constructor Direct injection