clean_helper 1.1.5
clean_helper: ^1.1.5 copied to clipboard
A CLI tool for scaffolding Flutter clean architecture projects.
clean_helper #
A Dart CLI tool that scaffolds Flutter projects following Clean Architecture.
Run it from inside a Flutter project root to generate the full directory structure, boilerplate files, routing, DI, BLoC state management, and all required dependencies.
Installation #
dart pub global activate --source git https://github.com/gktirkha/clean_helper
Enable Shell Auto-completion #
clean-helper install-completion-files
Commands #
| Command | Description |
|---|---|
clean-helper init |
Full project scaffold — run once on a new Flutter project |
clean-helper bootstrap |
pub get → slang → build_runner (re-bootstrap after git pull) |
clean-helper add-network-module |
Set up the network layer (Dio, Retrofit, Chucker) |
clean-helper add-auth-interceptor |
Scaffold AuthInterceptor with token refresh and wire into NetworkModule |
clean-helper add-feature <name> [--di] |
Add a new feature with clean architecture structure |
clean-helper add-repo <feature> <name> [--no-rest] |
Generate the full data layer (entity, domain repo, datasources, models, repo impl) |
clean-helper add-entity <scope> <name> [folder] |
Add an entity (domain) + freezed model (data) |
clean-helper build-runner [clean|build] |
Run build_runner in the current project (default: build) |
clean-helper remove-feature <name> |
Remove a feature and deregister its router |
clean-helper regenerate-router |
Scan all features on disk and regenerate app_router_module.dart |
clean-helper add-vscode-config |
Generate .vscode/extensions.json, launch.json, and tasks.json |
clean-helper generate-localizations |
Generate locales using slang |
clean-helper generate-tools [--overwrite] |
Generate tools/ scripts in the current project |
clean-helper list-mono-repo-apps |
List all apps declared in pubspec.yaml under clean-helper.mono_repo_apps |
<scope> for add-entity is either core or a feature name (e.g. home, auth). add-repo only supports feature scope.
Usage #
init — Bootstrap a Flutter project #
Run this once from the root of a new Flutter project:
cd my_flutter_app
clean-helper init
# Optional flags (can be combined)
clean-helper init --network # -n also set up the network layer
clean-helper init --di # -d generate a DI module for the home feature
clean-helper init --auth-interceptor # -a scaffold auth interceptor (implies --network)
clean-helper init --tools # -t generate tools/ scripts
What it does (in order):
- Validates
pubspec.yamlexists - Reads
name:frompubspec.yaml - Creates the full folder scaffold
- Generates localization files (
slang.yaml,assets/locales/en.locale.json) - Generates asset pipeline files (
build.yaml,assets/colors/colors.xml) - Scaffolds
packages/clean_routerlocal workspace package (CleanRouterBase+CleanRouterRefresh) - Patches root
pubspec.yamlwithworkspace: - packages/clean_router - Generates core Dart files (main, bootstrap, DI, routing, string extension)
- Generates utils (
Failure,AppLogger,Debouncer,getCurrentFunctionName,safeCast,safeExecute,listToModelList, type definitions) - Generates home feature scaffold
- Generates
tools/scripts (only with--tools) - Installs all runtime and dev dependencies
- Updates
.gitignore - Generates VSCode config (
.vscode/extensions.json,launch.json,tasks.json) - Patches
pubspec.yamlflutter assets - Sets up network layer (only with
--networkor--auth-interceptor) - Scaffolds auth interceptor (only with
--auth-interceptor) - Runs
dart run slang - Runs
dart run build_runner build - Runs
dart format .
Dependencies installed:
| Type | Packages |
|---|---|
| Runtime | flutter_bloc, go_router, get_it, injectable, freezed_annotation, fpdart, slang, slang_flutter, json_annotation, package_info_plus, flutter_svg, flutter_localizations (SDK), pretty_dio_logger (git), chucker_flutter (git) |
| Dev | build_runner, injectable_generator, freezed, flutter_gen_runner, json_serializable |
bootstrap — Re-bootstrap after git pull #
clean-helper bootstrap
Runs flutter pub get → dart run slang → dart run build_runner build in sequence. Use this after pulling changes that added new dependencies or modified generated files.
add-feature — Add a new feature #
clean-helper add-feature auth
clean-helper add-feature user_profile
clean-helper add-feature auth --di # also generate a DI module
Feature name must be snake_case. Generates:
lib/
├── app/navigation/
│ └── auth_navigation_impl.dart (@LazySingleton, implements AuthNavigation)
└── features/auth/
├── data/
│ ├── constants/
│ ├── datasources/
│ ├── models/
│ │ ├── requests/
│ │ └── response/
│ └── repositories/
├── di/ (only with --di flag)
│ └── auth_module.dart (@module abstract class AuthModule)
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── use_cases/
├── presentation/
│ ├── bloc/auth/
│ │ ├── auth_bloc.dart (@lazySingleton, extends Bloc)
│ │ ├── auth_event.dart (part of, @freezed)
│ │ └── auth_state.dart (part of, @freezed)
│ ├── pages/
│ │ └── auth_page.dart (pure UI widget)
│ ├── screens/
│ │ └── auth_screen.dart (BlocProvider wiring)
│ └── widgets/
└── router/
├── auth_routes.dart (sealed class AuthRoutes)
├── auth_navigation.dart (abstract class AuthNavigation)
└── auth_router.dart (@lazySingleton, implements CleanRouterBase)
The new feature router is automatically registered in lib/app/router/app_router_module.dart. dart format and build_runner run automatically at the end.
add-repo — Add a full data layer #
clean-helper add-repo home invoice
clean-helper add-repo auth user
clean-helper add-repo home invoice --no-rest # skip REST datasource and API paths
clean-helper add-repo home invoice --add-sample # generate get/post methods + request/response models
Generates the complete data layer for a repository inside lib/features/<feature>/:
domain/
entities/invoice_entity.dart (abstract class InvoiceEntity)
repositories/invoice_repository.dart (abstract interface)
data/
constants/home_api_paths.dart (sealed class HomeApiPaths)
datasources/invoice_data_source_base.dart (abstract interface)
datasources/rest_invoice_data_source.dart (@RestApi, @Injectable, Retrofit impl)
repositories/invoice_repository_impl.dart (@Singleton, implements InvoiceRepository)
With --add-sample: also adds get/post sample methods to all interfaces and creates models/requests/invoice_request_model.dart and models/response/invoice_response_model.dart.
--no-rest skips rest_invoice_data_source.dart and home_api_paths.dart, even if a network module is present. REST files are also skipped automatically if lib/core/network/di/network_module.dart does not exist.
All internal imports use relative paths. dart format and build_runner run automatically at the end.
add-entity — Add an entity + model #
# Feature scope
clean-helper add-entity home invoice
# With subfolder (places model in data/models/requests/)
clean-helper add-entity home invoice requests
# Core scope
clean-helper add-entity core error
Generates a domain entity and a freezed model:
// Domain
abstract class InvoiceEntity {}
// Data (freezed + json_serializable)
@freezed
sealed class InvoiceModel with _$InvoiceModel implements InvoiceEntity {
const factory InvoiceModel() = _InvoiceModel;
factory InvoiceModel.fromJson(Map<String, dynamic> json) =>
_$InvoiceModelFromJson(json);
}
dart format and build_runner run automatically at the end.
add-auth-interceptor — Scaffold the auth interceptor #
clean-helper add-auth-interceptor
Run this after add-network-module. Idempotent — skips anything already present. Does three things:
- Creates
lib/core/network/interceptors/auth_interceptor.dart— a@lazySingletonDio interceptor with:onRequest: attach Bearer token from storage (TODO to implement)onError: on 401, refresh the token and retry the original request- Refresh deduplication — concurrent 401s share a single refresh call
- Patches
lib/core/di/di_keys.dart— addsDIKeys.noAuthDioconstant - Patches
lib/core/network/di/network_module.dart:- Wires
AuthInterceptoras the first interceptor in the mainDioprovider - Adds a
noAuthDioprovider (Dio withoutAuthInterceptor) used byAuthInterceptorinternally to prevent infinite refresh loops
- Wires
build_runner runs automatically at the end. Fill in the TODOs in auth_interceptor.dart afterwards.
add-network-module — Set up the network layer #
clean-helper add-network-module
Generates Dio + Retrofit network files, installs network dependencies (including chucker_flutter), patches AppGoRouter, and runs build_runner automatically.
build-runner — Run build_runner #
clean-helper build-runner # build (default)
clean-helper build-runner build # build
clean-helper build-runner clean # clean generated files
remove-feature — Remove a feature #
clean-helper remove-feature auth
Deletes the feature directory and deregisters its router from lib/app/router/app_router_module.dart.
regenerate-router — Rebuild app_router_module.dart from scratch #
clean-helper regenerate-router
Scans lib/features/ for any feature that has a router/<feature>_router.dart file and regenerates lib/app/router/app_router_module.dart from scratch. Features are sorted alphabetically for deterministic output. app_router_module.dart is fully managed by the tool — do not edit it manually. Useful when the module has drifted out of sync or after manual edits to the features directory.
add-vscode-config — Generate VSCode configuration #
clean-helper add-vscode-config
Generates three files under .vscode/:
| File | Purpose |
|---|---|
extensions.json |
Recommended extensions for the project |
launch.json |
Debug launch configurations |
tasks.json |
Common project tasks |
All files are written with writeFile — skipped if they already exist. Also runs automatically as part of init.
generate-tools — Generate tools/ scripts #
clean-helper generate-tools # skip existing files
clean-helper generate-tools --overwrite # -o overwrite existing files
Generates helper shell scripts under tools/ in the current project. Use --overwrite to force-update scripts that already exist. Also available as part of init via the --tools flag.
generate-localizations — Generate locales #
clean-helper generate-localizations
Runs dart run slang to regenerate lib/generated/locales/locales.g.dart from assets/locales/en.locale.json.
list-mono-repo-apps — List declared mono-repo apps #
clean-helper list-mono-repo-apps
Reads clean-helper.mono_repo_apps from pubspec.yaml and prints every declared app. Useful for verifying monorepo configuration or debugging project detection.
Detected mono-repo apps (2):
1. app1 (apps/app1)
2. app2 (apps/app2)
If no apps are declared, prints setup instructions instead.
Generated Architecture #
Clean Architecture Structure #
Feature
├── domain/ Pure Dart — no Flutter, no packages
│ ├── entities/ Abstract classes (no JSON, no annotations)
│ ├── repositories/ Abstract interfaces
│ └── use_cases/ Business logic
│
└── data/ Implements domain contracts
├── models/ @freezed classes implementing entities
├── datasources/ API calls via Retrofit
└── repositories/ @LazySingleton implementations
Core Utils #
| File | Location |
|---|---|
AppLogger |
lib/core/utils/app_logger.dart |
Debouncer<T> |
lib/core/utils/debouncer.dart |
JsonDecodeFactory typedef |
lib/core/utils/type_definitions.dart |
safeCast |
lib/core/utils/functions/safe_cast.dart |
safeExecute |
lib/core/utils/functions/safe_execute.dart |
listToModelList |
lib/core/utils/functions/list_to_model_list.dart |
getCurrentFunctionName |
lib/core/utils/functions/get_current_function_name.dart |
Dependency Injection — get_it + injectable #
| Annotation | Meaning |
|---|---|
@lazySingleton |
Created on first access, shared instance |
@LazySingleton(as: X) |
Registered as abstract type X |
@Singleton(as: X) |
Eager singleton registered as X |
@module |
Provides third-party or platform instances |
@preResolve |
Awaited before app starts |
GetIt instance lives in lib/app/di/di_container.dart. @InjectableInit bootstraps everything via diInitializer(diContainer) in bootstrap.dart.
Routing — go_router + CleanRouterBase #
Router base classes live in the local packages/clean_router workspace package. Each feature provides a CleanRouterBase implementation:
abstract interface class CleanRouterBase {
List<RouteBase> get routes;
List<Stream<dynamic>> get refreshStreams;
FutureOr<String?> redirect(BuildContext context, GoRouterState state);
int get priority; // lower = higher priority
}
AppRouterModule collects all CleanRouterBase implementations, sorts by priority, and builds AppGoRouter. app_router_module.dart is tool-owned and regenerated automatically by add-feature, remove-feature, and regenerate-router.
State Management — flutter_bloc + freezed #
Every feature BLoC:
- Extends
Bloc<FeatureEvent, FeatureState> - Is annotated
@lazySingletonfor DI - Events and states are
@freezedunion types
Error Handling — fpdart + Failure #
FailureimplementsExceptionwith an optionalmessageFailure.leftFromError(e)wraps any caught object asLeft<Failure>safeCast<T>(data, decoder)— safely casts dynamic API responses toEither<Failure, T>safeExecute<T>(exec)— wraps any async call inEither<Failure, T>
Network — dio + retrofit #
Dioinstance provided byNetworkModule(@module)RetrofitLogger(@LazySingleton(as: ParseErrorLogger)) logs Retrofit parse errors viaAppLoggerErrorInterceptorparses{"errors": [...]}API responses intoErrorModelChuckerDioInterceptor+PrettyDioLoggeradded in all builds
Localization — slang #
- Config:
slang.yamlat project root - Source:
assets/locales/en.locale.json - Output:
lib/generated/locales/locales.g.dart String.trextension provides shorthand access to translations
Asset Generation — flutter_gen #
- Config:
build.yamlat project root - Output:
lib/generated/flutter_gen/ - Colors sourced from
assets/colors/colors.xml
Monorepo Support #
clean-helper works with monorepo Flutter workspaces. Run commands from the monorepo root — the tool detects the setup automatically.
Setup #
Add a clean-helper section to the root pubspec.yaml:
clean-helper:
mono_repo_apps:
- apps/app1
- apps/app2
How it works #
- Tool is run from the monorepo root (which has a
pubspec.yamlbut nolib/folder). - The configured apps are listed and you are prompted to choose one:
📦 Multiple mono-repo projects detected. Please select a project: 1. app1 (apps/app1) 2. app2 (apps/app2) Enter number (1–2): - After selection the command runs as if it were invoked from inside that app's directory — all generated files land in the correct location.
--scope flag #
Skip the interactive prompt by passing --scope=<app_name> before the subcommand:
clean-helper --scope=app1 init
clean-helper --scope=app1 add-feature login
clean-helper --scope=app2 add-repo home invoice
The flag matches on the folder name (last segment of the declared path). If two apps share the same name but have different paths, a narrowed prompt is shown for only those matches:
⚠️ Multiple apps named "app1" found. Please select one:
1. app1 (apps/app1)
2. app1 (packages/app1)
Enter number (1–2):
If no app matches, the command aborts and lists all available app names.
No config, no lib/? #
If the tool is run from a directory with no lib/ and no clean-helper.mono_repo_apps declaration, it exits with instructions to add the configuration.
Requirements #
- Dart SDK
^3.11.3 - Must be run from the root of a Flutter project (directory containing
pubspec.yaml), or from a monorepo root withclean-helper.mono_repo_appsdeclared