flutter_gen_cat 1.0.0 copy "flutter_gen_cat: ^1.0.0" to clipboard
flutter_gen_cat: ^1.0.0 copied to clipboard

FlutterGenCat is a CLI scaffold tool that brings MVVM and Clean Architecture into Flutter projects. Starts from plain MVVM and grows into Clean Architecture one flag at a time.

FlutterGenCat #

Dart SDK Flutter

A CLI that scaffolds MVVM and Clean Architecture features for Flutter apps.

Most architecture generators make you choose everything up front. FlutterGenCat starts you at plain MVVM and lets each feature earn its extra layers one flag at a time — so a counter screen stays six files, while a checkout flow gets the full domain layer.

Works with Provider, Riverpod and BLoC.


The name #

Cats settle into whatever shape the moment calls for. FlutterGenCat generates Flutter architecture the same way — freely and flexibly, taking the form each feature actually needs rather than one imposed up front.


Requirements #

Minimum
Dart SDK 3.5.0
Flutter 3.24.0

Flutter 3.24.0 is the stable release that shipped Dart 3.5.0 — the two bounds are the same requirement expressed twice. Only the Dart bound is declared in pubspec.yaml; FlutterGenCat is a pure Dart CLI and does not depend on the Flutter SDK to run.


Install #

dart pub global activate flutter_gen_cat

Or, to pin it to a project:

# pubspec.yaml
dev_dependencies:
  flutter_gen_cat: ^1.0.0
dart run flutter_gen_cat --help

Both flutter_gen_cat and the shorter fgc are installed.


Quick start #

# Plain MVVM, Provider — six files, no ceremony  (walked through in example/)
fgc create counter

# Riverpod, with a use case between the view model and the repository
fgc create cart -s riverpod --with-usecase

# The full Clean Architecture slice, on BLoC
fgc create checkout -s bloc --clean

# See what would happen, write nothing
fgc create orders --clean --dry-run

Generated files are written already formatted, at your project's own Dart language version — the first dart format after generation is a no-op.

See example/ for every file create counter produces, one layer at a time.


The progression #

The base is always the same six layers. Each flag inserts one more indirection:

adds why you would
(none) model · repository (+impl) · view model · view · injector The feature is a screen over an API.
--with-entity entity, mapper The wire format and the domain shape have started to diverge.
--with-usecase use case Business rules exist and do not belong in the view model.
--with-datasource data source (+impl) Transport is worth faking in tests, or there is more than one source.
--clean all three You already know this feature is complex.

Flags compose freely — --with-usecase alone is a perfectly reasonable place to sit for a long time.

Folder layout follows the layers #

Plain MVVM gets a flat layout, because there is nothing to group yet:

lib/features/counter/
  model/       counter_model.dart
  repository/  counter_repository.dart        # interface
               counter_repository_impl.dart
  view_model/  counter_view_model.dart
  view/        counter_view.dart
  di/          counter_injector.dart

Turn on any Clean Architecture layer and it switches to the layered one:

lib/features/checkout/
  data/
    model/        checkout_model.dart
    mapper/       checkout_mapper.dart
    datasource/   checkout_remote_data_source.dart
    repository/   checkout_repository_impl.dart
  domain/
    entity/       checkout_entity.dart
    repository/   checkout_repository.dart     # interface
    usecase/      get_checkout_use_case.dart
  presentation/
    view_model/   checkout_view_model.dart
    view/         checkout_view.dart
  di/             checkout_injector.dart

Override the choice with --layout flat or --layout layered whenever you disagree.

The repository interface lives in domain/ and the implementation in data/. That split is what keeps the dependency arrow pointing inwards: the presentation layer depends on the contract, and only the injector knows which class satisfies it.


Growing a feature #

add generates one layer into a feature that already exists. It reads the feature off disk first, so the new file matches the layout and the layers that are already there:

# Detects that `cart` is layered and has an entity
fgc add usecase clear_cart --feature cart
#   -> lib/features/cart/domain/usecase/clear_cart_use_case.dart
#      Future<CartEntity> call() => _repository.fetch();

# Detects that `counter` is flat and has no entity
fgc add usecase reset_counter --feature counter
#   -> lib/features/counter/usecase/reset_counter_use_case.dart
#      Future<CounterModel> call() => _repository.fetch();

A second model, view or data source in the same feature is named after the artifact rather than the feature:

fgc add model cart_summary --feature cart
#   -> lib/features/cart/model/cart_summary_model.dart   (class CartSummaryModel)

Explicit flags override what was detected. --layout is the only way to move a feature between layouts — adding --with-entity to a flat feature gives you an entity-shaped file in the flat layout, rather than silently scattering the feature across two conventions.


What you get per library #

The data and domain layers are identical for all three. Only the presentation three differ:

Provider Riverpod BLoC
View model ChangeNotifier AsyncNotifier Cubit<XState>
View StatelessWidget + context.watch ConsumerWidget + ref.watch BlocBuilder
Async state isLoading / error / data AsyncValue.when fields on the state class
Injector XInjector.provide(child:) top-level providers XInjector.provide(child:)

Mounting a feature:

// Provider and BLoC
CounterInjector.provide(child: const CounterView())

// Riverpod — the providers are top-level; just wrap the app once
ProviderScope(child: MaterialApp(home: CounterView()))

No view model imports material.dart, so all three are unit testable without a widget test.

Why Cubit rather than Bloc #

An MVVM view model is a state holder, and an event class per method buys nothing at scaffold time. Promote it to a full Bloc when the feature genuinely needs an event log — the state class is already there.


Command reference #

create <feature> #

-s, --state-management    provider (default) | riverpod | bloc
-o, --output              Root features directory (default: lib/features)
    --layout              flat | layered
    --with-entity         Split a domain entity out of the model, plus its mapper
    --with-usecase        Put a use case between the view model and the repository
    --with-datasource     Make the repository delegate transport to a data source
    --clean               All three of the above
    --usecase-name        Name of the generated use case (default: get_<feature>)
    --only                Generate only these layers, comma separated
    --dry-run             Print the plan, write nothing
    --overwrite           Replace files that already exist

add <layer> <name> #

Takes every option above, plus:

-f, --feature             Feature to add to (default: <name>)

Layers: model, entity, mapper, datasource, repository, repository-impl, usecase, viewmodel, view, injector.


Generated code is yours #

Files are written once and never touched again. There is no .g.dart, no build_runner step, no "do not edit" banner — create and add refuse to overwrite an existing file unless you pass --overwrite.

Fill in the TODO(FlutterGenCat) markers and the scaffold stops being a scaffold.


Using it as a library #

The CLI is a thin shell over an API that never touches the disk, which is handy if you are building your own tooling:

import 'package:flutter_gen_cat/flutter_gen_cat.dart';

void main() {
  final context = GenerationContext(
    feature: 'checkout',
    architecture: ArchitectureOptions.clean,
    stateManagement: StateManagement.riverpod,
    outputDir: 'lib/features', // the default
  );

  final files = FeatureScaffold(
    context,
    // Format for the target project rather than the newest language version
    // the bundled formatter knows — this is what the CLI does.
    languageVersion: resolveProjectLanguageVersion('.'),
  ).build();

  for (final file in files) {
    print('${file.path}\n${file.contents}');
  }
}

GenerationContext derives its FeaturePaths from the feature name, the output directory and the architecture's layout, so none of those has a second place to be set — and therefore no way to disagree with itself.


Development #

dart pub get
dart test
dart analyze
dart format .

License #

See LICENSE.

0
likes
160
points
99
downloads

Documentation

API reference

Publisher

verified publishercrossapplication.members.co.jp

Weekly Downloads

FlutterGenCat is a CLI scaffold tool that brings MVVM and Clean Architecture into Flutter projects. Starts from plain MVVM and grows into Clean Architecture one flag at a time.

Repository (GitHub)
View/report issues

Topics

#flutter #mvvm #clean-architecture #scaffold #cli

License

BSD-3-Clause (license)

Dependencies

args, dart_style, path, pub_semver, yaml

More

Packages that depend on flutter_gen_cat