flutter_riverpod_clean_generator
A code generator - not a runtime library - that scaffolds a feature-first
Clean Architecture project structure into a Flutter app, using Riverpod
Notifier/AsyncNotifier for state management.
Add it to a Flutter project, run it once (or once per feature), and it
writes core/ and features/<name>/ folders full of production-ready
Dart source directly into your lib/. Nothing from this package ships in
your app - once generation is done, your project depends only on
flutter_riverpod, go_router, dio, and shared_preferences.
Inspired by clean_gen_tool_plus,
but built around Riverpod instead of Bloc/Cubit, and safe by default: it
never overwrites a file that already exists unless you explicitly ask it
to, and it never touches your pubspec.yaml.
Install
dev_dependencies:
flutter_riverpod_clean_generator:
git:
url: <this repository>
(Or path: ../flutter_riverpod_clean_generator while developing locally, as
example/pubspec.yaml in this repo does.) It's a dev_dependency - it's a
build-time tool, nothing it exports is imported by your shipped app.
Quick start
Create lib/gen_tool.dart in your Flutter project:
import 'package:flutter_riverpod_clean_generator/flutter_riverpod_clean_generator.dart';
void main() async {
await CleanArchitectureGenerator.generate();
}
Run it:
dart run lib/gen_tool.dart
With no arguments, this walks you through an interactive wizard: whether
to generate core/, which features to add and what kind each one is,
whether to overwrite files that already exist, and the output directory.
Non-interactive / scriptable
Pass GeneratorOptions directly - this is also what every automated test
in this repo uses:
import 'package:flutter_riverpod_clean_generator/flutter_riverpod_clean_generator.dart';
void main() async {
await CleanArchitectureGenerator.generate(
options: GeneratorOptions(
includeCore: true,
features: [
FeatureConfig(name: 'auth', kind: FeatureKind.form),
FeatureConfig(name: 'dashboard', kind: FeatureKind.list),
],
),
);
}
CLI flags
For flag-driven usage, forward args into GeneratorOptions.fromArguments:
void main(List<String> args) async {
await CleanArchitectureGenerator.generate(
options: args.isEmpty ? null : GeneratorOptions.fromArguments(args),
);
}
dart run lib/gen_tool.dart --core --feature=auth:form --feature=dashboard:list
dart run lib/gen_tool.dart --feature=profile --dry-run # preview, writes nothing
dart run lib/gen_tool.dart --core --force # overwrite existing files
dart run lib/gen_tool.dart --help
| Flag | Meaning |
|---|---|
--core |
Generate the core/ layer |
--feature=name[:kind] |
Add a feature; repeatable. kind is entity, list, or form (default entity) |
--output=<dir> |
Output directory relative to the project root (default lib) |
--force, -f |
Overwrite files that already exist (default: skip them) |
--dry-run |
Compute and print the plan; write nothing |
--help, -h |
Print usage |
Tip: run
dart format .in your project after generating - like most generators, this one doesn't reformat on write. (Deliberately: this package's own tests usedart_styleto assert every template is valid Dart, butdart_stylestays a dev dependency here - pulling it in as a runtime dependency would drag inanalyzer, which needs a newermetathan the Flutter SDK pins, making the package unresolvable inside any Flutter project.)
Feature kinds
| Kind | Use for | Presentation state |
|---|---|---|
entity |
A single async resource (a profile, a settings blob) | AsyncNotifier's own AsyncValue<Entity> - no custom state class |
list |
A paginated collection (a product list, a feed) | AsyncNotifier<PaginatedState> with items/page/hasMore/isLoadingMore |
form |
A multi-field submission flow (login, sign-up, contact) | Notifier<FormState> with a submission-status enum |
Every kind produces the same layered shape (data/domain/presentation);
only the state, notifier, use case, page, and widget differ.
Generated project structure
lib/
├── core/
│ ├── constants/ app_constants.dart, api_constants.dart
│ ├── errors/ failure.dart, result.dart, app_exceptions.dart, exception_mapper.dart
│ ├── network/ api_client.dart (Dio), network_info.dart (connectivity_plus)
│ ├── routing/ app_router.dart (go_router) - add your feature routes here by hand
│ ├── theme/ app_colors.dart, app_text_styles.dart, app_theme.dart
│ ├── localization/ locale_provider.dart
│ ├── storage/ local_storage_service.dart (shared_preferences)
│ ├── utils/ usecase.dart (the shared UseCase<ResultType, Params> contract)
│ └── di/ core_providers.dart - cross-cutting infra only (Dio, storage, ...)
└── features/
└── <feature_name>/
├── data/
│ ├── datasources/ <feature>_remote_data_source.dart, <feature>_local_data_source.dart
│ ├── models/ <feature>_model.dart (fromJson/toJson/toEntity)
│ └── repositories/ <feature>_repository_impl.dart
├── domain/ (pure Dart - no Flutter or Riverpod import)
│ ├── entities/ <feature>_entity.dart
│ ├── repositories/<feature>_repository.dart
│ └── usecases/ get_<feature>_usecase.dart / submit_<feature>_usecase.dart / ...
└── presentation/
├── providers/ <feature>_providers.dart (DI wiring), <feature>_notifier.dart, <feature>_state.dart*
├── pages/ <feature>_page.dart
└── widgets/ <feature>_*_widget.dart
* only for list/form kinds.
See example/ for the real output of running this generator -
example/lib/core and example/lib/features/{auth,dashboard} are not
hand-written, they're what dart run lib/gen_tool.dart actually produced.
Design decisions
- Domain layer is pure Dart.
entities/,repositories/(interfaces), andusecases/never importflutterorflutter_riverpod.Result/Failure(core/errors/) are a small Dart 3 sealed-class pair, notdartz/fpdart. - No
@riverpodcode generation. EveryNotifier/AsyncNotifieris hand-written, wired via a plainNotifierProvider/AsyncNotifierProvider. Generated code compiles and runs immediately - nobuild_runnerstep. Dependencies are resolved withref.watch(...)insidebuild(), never through a constructor, since Riverpod recreates notifiers on rebuild. go_router, notauto_route- consistent with "no forced build step," and it's the Flutter-team-endorsed router.- Never touches your
pubspec.yaml. After generating, the console output lists the packages the generated code needs; you add them yourself. (The reference tool this was inspired by regenerates the wholepubspec.yamlunconditionally - this package deliberately never does that.) - Safe by default. Every file is checked against the filesystem before
writing: if it exists, it's skipped (not overwritten) unless you pass
overwrite: true/--force. AGenerationReporttells you exactly what was created, skipped, or overwritten. core/routing/app_router.dartis meant to be hand-edited afterward - add your feature'sGoRoutes there yourself once its pages exist. It's generated once and never depends on which features exist yet, so it's never regenerated out from under your edits unless you explicitly passoverwrite: true.
How it's built (for contributors)
lib/src/templates/- one small pure function per generated file (String buildXFile(...)), no hardcoded strings mixed into orchestration logic. Every template's output is asserted to be syntactically valid Dart intest/viapackage:dart_style'sDartFormatter.lib/src/engine/-core_generator.dart/feature_generator.dartturn options into a list ofPlannedFiles (no I/O beyond existence checks);file_writer.dartdecides create/skip/overwrite and performs the actual writes, recording per-file failures without aborting the run.lib/src/config/-GeneratorOptions,FeatureConfig/FeatureKind,GenerationPlan/GenerationReport,GeneratorException.lib/src/interaction/-Prompter(realStdinPrompter+ a fake queued-answer implementation used in tests) and console reporting.
Testing
dart pub get
dart analyze
dart test
39 tests cover: case-conversion utilities, CLI argument parsing, the
file-writer's create/skip/overwrite/partial-failure behavior, every core
and per-FeatureKind template (path layout + Dart-syntax validity), and
full end-to-end runs (including the interactive wizard via a fake
prompter, --dry-run, and rejecting a non-Flutter directory).
example/ is a real Flutter app whose lib/core and
lib/features/{auth,dashboard} were produced by actually running this
generator; flutter analyze and flutter test test/generated_smoke_test.dart
both pass against that output.
Credits
Created by odai0962 Contact: odai.zagha0962@gmail.com
License
MIT - see LICENSE.
Libraries
- flutter_riverpod_clean_generator
- A code generator that scaffolds a feature-first Clean Architecture
project structure with Riverpod (
Notifier/AsyncNotifier) into a Flutter project.