Getting Started

Flutist는 iOS 개발용 Tuist에서 영감을 받아 개발된 Flutter 애플리케이션을 위한 강력한 프로젝트 관리 프레임워크입니다. 이 프레임워크는 모듈식 아키텍처, 중앙 집중식 종속성 관리, 코드 생성 기능을 통해 대규모 Flutter 프로젝트를 체계적으로 관리할 수 있는 방안을 제공합니다.

Installation

Dart pub global로 설치합니다. Flutter SDK가 설치되어 있어야 합니다.

TERMINAL
$ dart pub global activate flutist
참고

flutist 명령어를 사용하려면 ~/.pub-cache/bin이 PATH에 등록되어 있어야 합니다.

Quick Start

  1. 초기화
    TERMINAL
    $ mkdir my_app && cd my_app
    $ flutist init

    y 입력 시 Flutter 프로젝트를 생성하고 Flutist 설정 파일을 추가합니다.

  2. 패키지 등록
    TERMINAL
    $ flutist pub add http bloc flutter_bloc

    버전을 자동으로 해결하여 package.dart에 추가합니다.

  3. 모듈 생성
    TERMINAL
    $ flutist create --name auth --path features --options clean

    auth_domain, auth_data, auth_presentation 3개 레이어를 자동 생성하고 project.dart에 의존성을 연결합니다.

  4. 전체 동기화
    TERMINAL
    $ flutist generate

    모든 pubspec.yaml이 즉시 동기화됩니다. 아키텍처 위반 시 이 단계에서 중단됩니다.

프로젝트 구조

FILE STRUCTURE
my_app/
  app/                         # 앱 셸 (simple 타입)
    lib/app.dart
    pubspec.yaml
  features/
    auth/
      auth_domain/             # UseCase, Entity, Repository interface
        lib/auth_domain.dart
        pubspec.yaml
      auth_data/               # Repository 구현체, API 클라이언트
        lib/auth_data.dart
        pubspec.yaml
      auth_presentation/       # UI, BLoC
        lib/auth_presentation.dart
        pubspec.yaml
  flutist/
    flutist_gen.dart           # ⚠ 자동 생성 — 수정 금지
    templates/
  lib/main.dart
  package.dart                 # 의존성 버전 레지스트리
  project.dart                 # 모듈 선언
  pubspec.yaml                 # 루트 workspace

Why Flutist?

모듈화는 대규모 Flutter 프로젝트의 표준 접근 방식입니다. 독립 빌드, 병렬 개발, 테스트 격리 — 효과는 분명합니다. 그러나 모듈이 늘어날수록 관리 비용도 함께 커집니다. Flutist는 이 관리 비용을 자동화로 제거합니다.

타입 안전한 의존성 관리

모듈이 10개 넘어가면 패키지 버전 불일치가 발생하기 쉽습니다. package.dart에 버전을 한 번 선언하면 flutist generateflutist_gen.dart를 자동 생성합니다. 이후 모든 모듈에서 IDE 자동완성과 타입 검사를 통해 안전하게 참조할 수 있습니다.

package.dart → flutist_gen.dart → project.dart
// 1. package.dart — 버전을 한 번만 선언
Dependency(name: 'dio', version: '^5.3.0'),
Dependency(name: 'flutter_bloc', version: '^8.1.6'),

// 2. flutist_gen.dart — flutist generate가 자동 생성
Dependency get dio         => dependencies.firstWhere((d) => d.name == 'dio');
Dependency get flutterBloc => dependencies.firstWhere((d) => d.name == 'flutter_bloc');
Module     get authDomain   => modules.firstWhere((m) => m.name == 'auth_domain');

// 3. project.dart — 타입 안전하게 참조 (IDE 자동완성 ✅)
Module(
  name: 'auth_data',
  dependencies: [package.dependencies.dio, package.dependencies.flutterBloc],
  modules: [package.modules.authDomain],
),

pubspec.yaml 중앙 관리

모듈 수만큼 늘어나는 pubspec.yaml. 하나의 패키지 버전을 올리려면 이를 참조하는 모든 파일을 직접 수정해야 합니다.

Flutist는 project.dart 선언을 기반으로 모든 pubspec.yaml을 자동 동기화합니다. 개발자가 직접 수정하는 건 project.dart 단 한 파일입니다.

TERMINAL
$ flutist generate
✓ pubspec.yaml 동기화: app, auth_domain, auth_data, auth_presentation,
                      product_interface, product_implementation ... (총 24개)
✓ 모든 아키텍처 규칙 통과
✓ 완료 (0.8s)

아키텍처 규칙 자동화

문서나 코드 리뷰만으로는 아키텍처 규칙을 일관되게 유지하기 어렵습니다. 바쁜 개발 과정에서 domainhttp를 import하거나, 다른 피처의 구현체를 직접 참조하는 일이 생깁니다. 이런 위반은 발견하기 어렵고, 발견했을 때는 이미 여러 곳에 퍼진 경우가 많습니다.

Flutist는 아키텍처 규칙을 실행 가능한 코드로 만듭니다. strictMode: true(기본값)에서 위반이 감지되면 generate가 즉시 중단됩니다. 문서에만 존재하던 원칙이 빌드 게이트가 됩니다.

TERMINAL — 위반 감지 예시
$ flutist generate
✗ [B4] auth_domain → auth_data: 역방향 의존성 감지 — domain은 data를 알 수 없음
   → auth_domain에서 Dio import를 제거하고 Repository interface만 선언하세요
✗ generate 중단 (strictMode: true)

새로 합류한 팀원이 아키텍처를 완전히 이해하지 못해도, 규칙을 어기는 순간 명확한 피드백을 받습니다. 코드 리뷰에서 아키텍처 위반을 지적하는 대신, 도구가 자동으로 검사합니다.

보일러플레이트 자동 생성

Modular Architecture의 가장 큰 불편함 중 하나는 보일러플레이트입니다. 피처 하나를 추가할 때마다 interface, implementation, testing, tests, example 패키지를 만들고, 각각의 pubspec.yaml, lib/ 구조, 배럴 파일까지 세팅해야 합니다.

BLoC처럼 파일이 많은 상태관리 패턴은 피처 하나에 이벤트, 상태, BLoC, 페이지, 위젯 파일이 모두 필요합니다. flutist createflutist scaffold로 이 모든 것을 명령어 한 번에 생성합니다.

TERMINAL
# micro 타입으로 todos 피처 생성 — 5개 패키지 + 구조 전체 자동화
$ flutist create --name todos --path features --options micro
✓ features/todos/todos_interface      생성
✓ features/todos/todos_implementation 생성
✓ features/todos/todos_testing        생성
✓ features/todos/todos_tests          생성
✓ features/todos/todos_example        생성

# BLoC scaffold로 모든 파일 생성
$ flutist scaffold --template bloc --name todos_overview --path features/todos/todos_implementation
✓ todos_overview_bloc.dart  생성
✓ todos_overview_event.dart 생성
✓ todos_overview_state.dart 생성

package.dart

프로젝트에서 사용하는 모든 외부 패키지 버전과 모듈 이름을 한 곳에서 선언합니다. 버전은 이 파일에서만 관리됩니다.

package.dart
import 'package:flutist/flutist.dart';

final package = Package(
  name: 'my_app',

  dependencies: [
    Dependency(name: 'http',         version: '^1.1.0'),
    Dependency(name: 'bloc',         version: '^8.1.0'),
    Dependency(name: 'flutter_bloc',  version: '^8.1.0'),
    Dependency(name: 'test',         version: '^1.24.0'),
    Dependency(name: 'mockito',      version: '^5.4.4'),
  ],

  modules: [
    Module(name: 'app'),
    Module(name: 'auth_domain'),
    Module(name: 'auth_data'),
    Module(name: 'auth_presentation'),
  ],
);
주의

파싱은 Regex 기반입니다. Module(name: 'foo')를 한 줄에 쓰면 인식되지 않습니다. 반드시 멀티라인 형식을 사용하세요.

project.dart

각 모듈의 의존성과 모듈 간 관계를 선언합니다. flutist generate가 이 파일을 읽어 모든 pubspec.yaml을 동기화합니다.

project.dart
import 'package:flutist/flutist.dart';
import 'flutist/flutist_gen.dart';
import 'package.dart';

final project = Project(
  name: 'my_app',

  options: const ProjectOptions(
    strictMode: true,            // 위반 시 generate 중단
    compositionRoots: ['app'], // _implementation 직접 참조 허용 모듈
  ),

  modules: [
    Module(
      name: 'auth_domain',
      dependencies: [],
      devDependencies: [package.dependencies.test],
      modules: [],
    ),
    Module(
      name: 'auth_data',
      dependencies: [package.dependencies.http],
      devDependencies: [package.dependencies.test, package.dependencies.mockito],
      modules: [package.modules.authDomain],
    ),
    Module(
      name: 'auth_presentation',
      dependencies: [package.dependencies.bloc, package.dependencies.flutterBloc],
      devDependencies: [package.dependencies.test],
      modules: [package.modules.authDomain],
    ),
    Module(
      name: 'app',
      modules: [package.modules.authPresentation],
    ),
  ],
);

Module 필드

필드타입설명
nameString모듈 이름. package.dart의 modules 목록에 있어야 함
dependenciesList<Dependency>외부 패키지 의존성 (package.dependencies.xxx)
devDependenciesList<Dependency>개발용 의존성 (test, mockito 등)
modulesList<Module>의존하는 다른 모듈 (package.modules.xxx)

flutist_gen.dart

flutist generate 실행 시 자동 생성됩니다. package.dependencies.http, package.modules.authDomain처럼 IDE 자동완성이 동작하는 타입 세이프 접근자를 제공합니다. 직접 수정하지 마세요.

flutist/flutist_gen.dart (자동 생성)
// ⚠️ Auto-generated by flutist. Do not modify.

extension PackageDependenciesX on List<Dependency> {
  Dependency get http        => firstWhere((d) => d.name == 'http');
  Dependency get bloc        => firstWhere((d) => d.name == 'bloc');
  Dependency get flutterBloc => firstWhere((d) => d.name == 'flutter_bloc');
  Dependency get test        => firstWhere((d) => d.name == 'test');
}

extension PackageModulesX on List<Module> {
  Module get app              => firstWhere((m) => m.name == 'app');
  Module get authDomain       => firstWhere((m) => m.name == 'auth_domain');
  Module get authData         => firstWhere((m) => m.name == 'auth_data');
  Module get authPresentation => firstWhere((m) => m.name == 'auth_presentation');
}

Module Type: clean

Clean Architecture 기반 3-layer 피처 모듈. UI, 비즈니스 로직, 데이터를 엄격하게 분리합니다.

presentation
UI, State Management (BLoC, Riverpod)
↓ depends on
data
Repository 구현체, API 클라이언트
↓ depends on
domain
Entity, UseCase, Repository interface — 외부 의존 없음
TERMINAL
$ flutist create --name auth --path features --options clean

자동 연결되는 project.dart 항목

project.dart (자동 추가)
Module(name: 'auth_domain', modules: []),
Module(
  name: 'auth_data',
  modules: [package.modules.authDomain],   # data → domain
),
Module(
  name: 'auth_presentation',
  modules: [package.modules.authDomain],   # presentation → domain (data 아님)
),

Module Type: micro

Microfeature Architecture 기반 5-layer. 외부에서 재사용되는 라이브러리 모듈에 적합합니다. 소비자는 오직 interface에만 의존합니다.

example
사용 예시 앱 (lib/main.dart 포함)
tests
단위/통합 테스트 (test/ 디렉토리)
testing
Mock, Fake, Stub — 프로덕션 코드 유입 금지
implementation
구체적 구현체 — composition root에서만 주입
↓ depends on
interface
공개 API 계약 (abstract class, interface, model)
TERMINAL
$ flutist create --name network --path packages --options micro

Module Type: lite

micro에서 example을 제거한 4-layer. 데모 앱이 필요 없는 내부 API 모듈에 적합합니다.

tests
단위/통합 테스트
testing
테스트 더블 (Mock, Fake)
implementation
구체적 구현체
↓ depends on
interface
공개 API 계약
TERMINAL
$ flutist create --name storage --path packages --options lite

Module Type: simple

단일 패키지. 레이어 분리 없이 유틸리티, 공유 모델, 앱 셸에 사용합니다.

TERMINAL
$ flutist create --name utils --path packages
FILE STRUCTURE
packages/utils/
  lib/utils.dart
  pubspec.yaml
  analysis_options.yaml
  README.md

flutist init

Flutist 프로젝트를 초기화합니다. 신규 프로젝트와 기존 프로젝트 마이그레이션 두 흐름을 지원합니다.

$ flutist init

신규 프로젝트 (pubspec.yaml 없음)

TERMINAL
$ mkdir my_app && cd my_app
$ flutist init

Do you want to create a new Flutter project? (y/n): y
▶ flutter create . --project-name my_app
▶ project.dart / package.dart 생성
▶ app/ 모듈 생성 (simple 타입)
▶ flutist_gen.dart 생성

기존 프로젝트 마이그레이션

TERMINAL
$ cd existing_project && flutist init

1) New project  2) Migration?: 2
▶ project.dart / package.dart 생성 (기존 pubspec.yaml 보존)
▶ flutist_gen.dart 생성

flutist create

새 모듈을 생성합니다. 디렉토리, pubspec.yaml, barrel 파일을 자동으로 만들고 project.dart, package.dart에 자동 등록합니다.

$ flutist create --name <name> --path <path> [--options <type>]
옵션단축키필수설명
--name-n모듈 이름 (snake_case)
--path-p생성할 디렉토리 경로
--options-oclean · micro · lite (미지정 시 simple)
TERMINAL
$ flutist create --name auth --path features --options clean
$ flutist create --name network --path packages --options micro
$ flutist create --name storage --path packages --options lite
$ flutist create --name utils --path packages
주의

모듈 이름에 _domain, _implementation 등 레이어 suffix를 직접 붙이지 마세요. Flutist가 자동으로 처리합니다.

flutist generate

project.dart를 읽어 모든 모듈의 pubspec.yaml을 동기화합니다. 아키텍처 규칙 검사도 함께 실행됩니다.

$ flutist generate
  1. package.dart 파싱

    의존성 목록과 모듈 이름 목록을 읽습니다.

  2. project.dart 파싱

    모듈 설정, strictMode, compositionRoots를 읽습니다.

  3. 아키텍처 규칙 검사

    5가지 규칙 실행. strictMode: true이면 위반 시 exit 1.

  4. flutist_gen.dart 재생성

    타입 세이프 접근자 코드를 갱신합니다.

  5. pubspec.yaml 전체 동기화

    SDK 의존성과 주석을 보존하며 dependencies/path 참조를 업데이트합니다.

flutist check

파일 생성 없이 아키텍처 규칙만 검사합니다. CI 파이프라인에 추가하기 적합합니다.

$ flutist check
TERMINAL — 출력 예시
$ flutist check

✓ [B1] Implementation references: OK
✓ [B2] Testing layer references: OK
✓ [B3] Example layer references: OK
✗ [B4] auth_domain → auth_data: domain must not depend on data
✓ [B5] Circular dependencies: OK

flutist pub

외부 패키지를 package.dart에 추가합니다. dart pub add로 버전을 자동 해결합니다.

$ flutist pub add <package> [<package2> ...]
TERMINAL
$ flutist pub add http
$ flutist pub add bloc flutter_bloc freezed

추가 후 package.dartDependency 항목이 삽입되고 flutist_gen.dart가 재생성됩니다. 이후 package.dependencies.http로 접근할 수 있습니다.

flutist scaffold

템플릿으로 저장하고 재사용합니다. 자세한 내용은 아래 Scaffold 섹션을 참고하세요.

$ flutist scaffold <template> --name <name> [options]
TERMINAL
$ flutist scaffold list
$ flutist scaffold bloc_feature --name login
$ flutist scaffold bloc_feature --name login --path lib/features

flutist test

모든 모듈의 테스트를 병렬로 실행합니다. Flutter 의존성 유무를 자동으로 감지해 flutter test 또는 dart test를 선택합니다.

$ flutist test [--module <name>]
TERMINAL
$ flutist test
$ flutist test --module auth_domain

flutist graph

모듈 간 의존성 그래프를 시각화합니다.

$ flutist graph [--format <fmt>] [--output <file>] [--open]
옵션기본값설명
--formatmermaidmermaid · dot · ascii
--output-출력 파일 경로
--openfalse브라우저에서 바로 열기 (mermaid)
TERMINAL
$ flutist graph --open
$ flutist graph --format dot --output graph.dot
$ flutist graph --format ascii

Architecture

Flutist는 아키텍처 규칙을 flutist generate·flutist check 단계에서 자동으로 검사합니다. 문서에만 존재하던 원칙이 실행 가능한 코드가 됩니다.

Microfeature Architecture

Microfeature Architecture는 하나의 기능(피처)을 역할이 명확한 5개 레이어로 쪼개 각각 독립 패키지로 만드는 설계 패턴입니다. iOS 생태계의 Tuist, Point-Free의 모듈화 가이드라인에서 발전했으며, 대규모 Flutter 프로젝트에서 완전한 모듈 격리가 필요할 때 사용됩니다.

핵심 아이디어는 하나입니다 — 소비자는 interface만 알고, 구현체는 composition root가 주입한다. 이 원칙 하나로 의존성 방향이 일관되고, 테스트가 격리되고, 피처 간 결합이 제거됩니다.

레이어 구조

interface
공개 계약. 모델, abstract class, 예외. 소비자가 유일하게 의존하는 레이어.
implementation
구현 세부사항. Repository 구현체, BLoC, UI. composition root와 같은 피처의 tests·example만 참조 가능.
testing
테스트 더블. Fake, Mock, Stub. 프로덕션 의존성에는 절대 유입되지 않음.
tests
단위·위젯 테스트. implementation과 testing을 함께 참조하여 격리된 테스트.
example
독립 데모 앱. implementation·testing을 직접 조립. 의존성 그래프의 끝에 위치.

장점

  • 소비자 격리 — 소비자 모듈은 interface 패키지만 알면 됩니다. 구현체가 교체되어도 영향 없음.
  • 테스트 속도 — Fake로 외부 의존성 없이 BLoC·Repository를 즉시 테스트.
  • 피처 독립 실행 — _example 앱으로 피처를 앱 없이 단독 실행·확인.
  • 점진적 교체 — implementation만 교체하면 나머지 레이어는 그대로 유지.

Implementation 참조 제한

implementation은 내부 구현 세부사항입니다. 소비자 모듈이 구현체를 직접 알게 되면 Microfeature의 격리 원칙이 무너집니다. 구현체는 오직 composition root(보통 app)가 의존성을 주입하는 방식으로만 사용됩니다.

ERRORimplementation 레이어 직접 참조 금지
composition root, 같은 피처의 example·tests를 제외한 모든 모듈에서 implementation 직접 참조는 아키텍처 위반입니다.

허용

  • app (composition root) → network_implementation — 구현체 주입 지점
  • network_examplenetwork_implementation — 같은 피처 데모 앱
  • network_testsnetwork_implementation — 같은 피처 테스트

금지

  • product_interfacenetwork_implementation — 소비자는 interface만
  • cart_implementationnetwork_implementation — 다른 피처의 구현체 직접 참조
  • auth_presentationstorage_implementation — 비-composition root
compositionRoots 설정

implementation을 직접 참조할 수 있는 모듈은 ProjectOptions.compositionRoots로 지정합니다. 기본값은 ['app']입니다. 멀티-앱 구조에서는 여러 값을 선언할 수 있습니다.

project.dart
options: const ProjectOptions(
  compositionRoots: ['app', 'dev_app'],  // 여러 composition root 지정 가능
),

Testing 레이어 격리

testing은 Fake·Mock·Stub을 담는 전용 패키지입니다. 이 레이어의 존재 의미는 "테스트 더블을 프로덕션 코드 없이 재사용할 수 있는 단위로 배포한다"는 것입니다. 반대로 말하면, 프로덕션 코드에 testing이 유입되는 순간 이 격리는 깨집니다.

ERRORtesting 레이어 프로덕션 코드 유입 금지
Mock, Fake, Stub은 테스트와 예시 코드에만 존재해야 합니다. 프로덕션 의존성에 testing이 포함되면 릴리즈 빌드에 테스트용 코드가 포함됩니다.

허용

  • network_testsnetwork_testing — 테스트 코드에서 Mock 사용
  • network_examplenetwork_testing — 데모 앱에서 Fake 사용
  • product_testsproduct_testing — 피처 테스트에서 Fake 사용

금지

  • appnetwork_testing — 프로덕션 앱에 Mock 유입
  • cart_implementationnetwork_testing — 프로덕션 구현체에 Fake 유입

Example 독립성

example은 의존성 그래프의 리프 노드입니다. 피처를 앱 없이 독립 실행하여 확인하는 진입점이며, 어떤 모듈도 이를 의존성으로 가져서는 안 됩니다. example이 다른 모듈의 의존성이 되는 순간 프로덕션 빌드에 데모 코드가 포함됩니다.

ERRORexample 레이어는 어떤 의존성으로도 참조 불가
example은 항상 의존성 화살표를 받는 쪽(수신자)입니다. 화살표를 내보내는 쪽(공급자)이 될 수 없습니다.

금지

  • appnetwork_example — 프로덕션 앱이 데모 앱에 의존
  • product_implementationnetwork_example — 구현체가 데모에 의존
  • cart_testsproduct_example — 테스트가 다른 피처 데모에 의존

Clean Architecture

Clean Architecture는 Robert C. Martin(Uncle Bob)이 정의한 아키텍처 원칙으로, 비즈니스 로직을 UI·데이터베이스·프레임워크로부터 완전히 분리하는 것을 목표로 합니다. Flutter에서는 피처의 비즈니스 규칙(domain)이 HTTP 클라이언트나 Flutter 위젯이 무엇인지 알지 못하도록 설계합니다.

핵심은 의존성 방향입니다 — 모든 화살표는 domain을 향합니다. domain은 외부를 전혀 모릅니다. data와 presentation이 domain에 의존하고, domain은 아무것에도 의존하지 않습니다.

레이어 구조

domain
비즈니스 핵심. Entity, UseCase, Repository interface. Flutter·http 패키지를 import하지 않습니다. 가장 안정적인 레이어.
data
↑ domain
데이터 소스 구현. Repository 구현체, API 클라이언트, 로컬 스토리지. domain의 interface를 구현합니다.
presentation
↑ domain
UI·상태관리. BLoC, ViewModel, Widget. domain의 UseCase·Entity를 사용합니다. data에도 의존할 수 있습니다.

장점

  • 비즈니스 로직 독립성 — domain 레이어는 Flutter SDK 없이 순수 Dart로 테스트 가능.
  • 데이터 소스 교체 — REST API → GraphQL, SQLite → Hive 교체 시 data 레이어만 변경.
  • 명확한 책임 분리 — "이 코드가 어디 있어야 하는가?" 질문에 항상 명확한 답이 있음.
  • 테스트 용이성 — domain은 mock 없이 테스트, presentation은 Repository mock으로 테스트.

레이어 방향 강제

Clean Architecture에서 가장 흔히 발생하는 실수는 역방향 의존성입니다. 바쁜 개발 중에 domain이 HTTP 클라이언트를 직접 import하거나, data가 UI 위젯을 알게 되는 일이 일어납니다. Flutist는 모듈 이름의 suffix(domain·data·presentation)를 기준으로 같은 피처 내의 방향 위반을 generate 시점에 자동 감지합니다.

ERRORdomain ← data, domain ← presentation 방향 강제
domain은 아무것도 참조하지 않습니다. data는 presentation을 모릅니다. 이 두 규칙은 절대적입니다.

허용 (올바른 방향)

  • weather_dataweather_domain — data가 domain 계약을 구현
  • weather_presentationweather_domain — UI가 domain UseCase 사용
  • weather_presentationweather_data — presentation이 data에 의존 (허용)

금지 (역방향)

  • weather_domainweather_data — domain이 구현체를 알게 됨
  • weather_domainweather_presentation — domain이 UI를 알게 됨
  • weather_dataweather_presentation — data가 UI를 알게 됨
감지 방법

Flutist는 project.dart의 modules 선언을 기준으로 검사합니다. 실제 Dart import가 아닌 모듈 의존성 선언 레벨에서 방향을 강제합니다.

순환 의존성

순환 의존성은 두 아키텍처 모두에서 금지됩니다. A → B → C → A 형태의 순환이 발생하면 빌드 시스템이 의존성 순서를 결정할 수 없고, 모듈을 독립적으로 테스트하거나 배포하는 것이 불가능해집니다. Flutist는 DFS(깊이 우선 탐색) 기반으로 모든 모듈 의존성 그래프를 순회하며 순환을 탐지합니다.

ERROR모듈 그래프 내 순환 의존성 금지
직접 순환(A → B → A)과 간접 순환(A → B → C → A) 모두 탐지합니다.

금지

  • auth_domain → utils → auth_domain
  • product_interface → cart_interface → product_interface
  • A → B → C → A (간접 순환)
디버깅 팁

순환이 의심될 때는 flutist graph --format ascii로 의존성 방향을 시각화하여 확인하세요.

strictMode

위에서 설명한 모든 규칙은 flutist generate·flutist check 실행 시 항상 검사됩니다. strictMode는 위반을 발견했을 때 중단할지 경고만 출력할지를 제어합니다. 기존 프로젝트를 Flutist로 마이그레이션하는 기간 동안 false로 설정하면 규칙을 위반한 채로 진행하면서 점진적으로 수정할 수 있습니다.

설정위반 시 동작권장 용도
true (기본)generate 즉시 중단, exit 1일반 개발, CI/CD
false경고 출력 후 계속, exit 0기존 프로젝트 마이그레이션
project.dart
options: const ProjectOptions(
  strictMode: false,  // 마이그레이션 기간 동안 임시 비활성화
  compositionRoots: ['app'],
),
주의

strictMode: false는 마이그레이션을 위한 임시 설정입니다. 완료 후 반드시 true로 되돌리세요.

Scaffold Templates

템플릿으로 저장하고 flutist scaffold로 반복 작업을 자동화합니다.

템플릿 변수

변수입력: login_feature출력
{{name | snake_case}}login_featurelogin_feature
{{name | pascal_case}}LoginFeature
{{name | camel_case}}loginFeature
{{name | upper_case}}LOGIN_FEATURE

template.yaml 구조

flutist/templates/<name>/template.yaml
description: "BLoC Feature Template"
attributes:
  - name: name
    required: true
  - name: path
    required: false
    default: "lib/features"
items:
  - type: file
    path: "{{path}}/{{name | snake_case}}/{{name | snake_case}}_bloc.dart"
    templatePath: "bloc.dart.template"
  - type: string
    path: "{{path}}/{{name | snake_case}}/README.md"
    contents: |
      # {{name | pascal_case}}

item 타입

타입설명
file.template 파일을 읽어 변수 치환 후 생성
string인라인 내용으로 파일 생성
directory템플릿 디렉토리 전체 복사

실전 예시: BLoC Feature 템플릿

flutist/templates/bloc_feature/bloc.dart.template
import 'package:bloc/bloc.dart';

part '{{name | snake_case}}_event.dart';
part '{{name | snake_case}}_state.dart';

class {{name | pascal_case}}Bloc
    extends Bloc<{{name | pascal_case}}Event, {{name | pascal_case}}State> {
  {{name | pascal_case}}Bloc() : super(const {{name | pascal_case}}Initial()) {
    on<{{name | pascal_case}}Started>(_onStarted);
  }
  Future<void> _onStarted({{name | pascal_case}}Started event, Emitter<{{name | pascal_case}}State> emit) async {}
}
flutist/templates/bloc_feature/event.dart.template
part of '{{name | snake_case}}_bloc.dart';

sealed class {{name | pascal_case}}Event { const {{name | pascal_case}}Event(); }

final class {{name | pascal_case}}Started extends {{name | pascal_case}}Event {
  const {{name | pascal_case}}Started();
}
flutist/templates/bloc_feature/state.dart.template
part of '{{name | snake_case}}_bloc.dart';

sealed class {{name | pascal_case}}State { const {{name | pascal_case}}State(); }

final class {{name | pascal_case}}Initial extends {{name | pascal_case}}State { const {{name | pascal_case}}Initial(); }
final class {{name | pascal_case}}Loading extends {{name | pascal_case}}State { const {{name | pascal_case}}Loading(); }
final class {{name | pascal_case}}Loaded<T> extends {{name | pascal_case}}State {
  final T data; const {{name | pascal_case}}Loaded(this.data);
}
final class {{name | pascal_case}}Error extends {{name | pascal_case}}State {
  final String message; const {{name | pascal_case}}Error(this.message);
}
TERMINAL — 실행 결과
$ flutist scaffold bloc_feature --name login --path lib/features

✓ lib/features/login/login_bloc.dart
✓ lib/features/login/login_event.dart
✓ lib/features/login/login_state.dart

실전 예시: Riverpod Notifier 템플릿

flutist/templates/riverpod_feature/notifier.dart.template
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '{{name | snake_case}}_state.dart';

part '{{name | snake_case}}_notifier.g.dart';

@riverpod
class {{name | pascal_case}}Notifier extends _${{name | pascal_case}}Notifier {
  @override
  {{name | pascal_case}}State build() => const {{name | pascal_case}}State.initial();

  Future<void> load() async {
    state = const {{name | pascal_case}}State.loading();
    try {
      state = const {{name | pascal_case}}State.loaded(null);
    } catch (e) {
      state = {{name | pascal_case}}State.error(e.toString());
    }
  }
}
flutist/templates/riverpod_feature/state.dart.template
import 'package:freezed_annotation/freezed_annotation.dart';

part '{{name | snake_case}}_state.freezed.dart';

@freezed
class {{name | pascal_case}}State with _${{name | pascal_case}}State {
  const factory {{name | pascal_case}}State.initial()          = _Initial;
  const factory {{name | pascal_case}}State.loading()          = _Loading;
  const factory {{name | pascal_case}}State.loaded(dynamic data) = _Loaded;
  const factory {{name | pascal_case}}State.error(String msg)   = _Error;
}