composable_architecture_generator

Code generator for the Composable Architecture in Dart. Automatically generates Lens, ActionLens, Prism, and utility extensions from annotations provided by composable_architecture_core.

Installation

Add to your pubspec.yaml:

dependencies:
  composable_architecture_core: ^0.1.0

dev_dependencies:
  composable_architecture_generator: ^0.2.0
  build_runner: ^2.13.0

Run code generation:

dart run build_runner build

Annotations

@tcaState

Generates a Lens for every field in the annotated class, enabling modular state composition via Reducer.pullback.

@tcaState
class CounterState {
  final int count;
  final String label;

  const CounterState({required this.count, required this.label});

  CounterState copyWith({int? count, String? label}) =>
      CounterState(count: count ?? this.count, label: label ?? this.label);
}

Generated output:

class CounterStateLens {
  static Lens<CounterState, int> count = (
    get: (state) => state.count,
    set: (state, count) => state.copyWith(count: count),
  );

  static Lens<CounterState, String> label = (
    get: (state) => state.label,
    set: (state, label) => state.copyWith(label: label),
  );
}

List and Set fields also get an Iterable variant lens (e.g. itemsIterable) for use with ForEachIterableReducer.

@tcaAction

Generates ActionLens / Prism for child-action parameters, and utility extensions for pattern matching on sealed action classes.

@tcaAction
sealed class AppAction {
  const AppAction();
}

class AppActionIncrement extends AppAction {
  const AppActionIncrement();
}

class AppActionSetLabel extends AppAction {
  final String label;
  const AppActionSetLabel(this.label);
}

With freezed

When using @freezed, the generator uses the declared subclasses from the redirecting factory constructors:

@tcaAction
@freezed
sealed class AppAction with _$AppAction {
  const factory AppAction.increment() = AppActionIncrement;
  const factory AppAction.setLabel(String label) = AppActionSetLabel;
}

With dart_mappable

When using @MappableClass(), the generator creates concrete subclasses automatically from factory constructors:

@tcaAction
@MappableClass(discriminatorKey: 'type')
sealed class AppAction with AppActionMappable {
  const AppAction();
  const factory AppAction.increment() = AppActionIncrement;
  const factory AppAction.setLabel(String label) = AppActionSetLabel;
}

Generated utilities

Optionally generates an extension on the sealed action class with helpers for working with subclasses without manual is / as casts:

Method Description
mapEvery<T> Exhaustive pattern match — every subclass must be handled. Returns T.
mapAny<T> Partial match with a required orElse fallback. Returns T.
mapAnyOrNull<T> Partial match returning T? — unhandled cases return null.
onEvery Like mapEvery but returns void — for side effects.
onAny Like mapAny but returns void.
is<Subclass> Type-check getters (e.g. isAppActionIncrement).
<factoryName> Nullable cast getters (e.g. action.increment returns AppActionIncrement?).
extension AppActionUtils on AppAction {
  T mapEvery<T>({
    required T Function(AppActionIncrement) increment,
    required T Function(AppActionSetLabel) setLabel,
  }) => ...;

  T mapAny<T>({required T Function() orElse, ...}) => ...;
  T? mapAnyOrNull<T>({...}) => ...;
  void onEvery({...}) => ...;
  void onAny({...}) => ...;

  bool get isAppActionIncrement => ...;
  bool get isAppActionSetLabel => ...;

  AppActionIncrement? get increment => ...;
  AppActionSetLabel? get setLabel => ...;
}

Utility generation is disabled by default. To enable it per-class:

@TCAAction(generateUtils: true)
sealed class AppAction { ... }

Or enable it globally via build.yaml:

targets:
  $default:
    builders:
      composable_architecture_generator:
        options:
          generate_utils: true

Local annotation always takes priority over the global option. The resolution order is: local annotation > global option > default (false).

ActionLens & Prism

When a subclass parameter is itself annotated with @tcaAction, the generator produces an ActionLens (single child-action parameter) or Prism (child-action + ID parameter for collections):

@tcaAction
sealed class ParentAction {
  const ParentAction();
}

class ParentActionChild extends ParentAction {
  final ChildAction childAction;
  const ParentActionChild(this.childAction);
}

Generated:

class ParentActionLens {
  static ActionLens<ParentAction, ChildAction> child = (
    extract: (action) => action is ParentActionChild ? action.childAction : null,
    embed: (value) => ParentAction.child(value),
  );
}

@tcaRoute

Generates RouteLens and Prism for classes extending Routable, used with composable_architecture_router for Navigator 2.0 integration.

@tcaRoute
class MyRoute extends Routable<AppState, RouteState, LocalState?, AppAction, LocalAction> {
  // implement buildLocalState, setBackFromLocalState, extractAction, toAppAction
}

Generated:

extension on MyRoute {
  RouteLens<AppState, RouteState, LocalState?> get routeLens => ...;
  Prism<AppAction, LocalAction, RouteID> get actionPrism => ...;
}

Requirements

  • Dart SDK >=3.1.2 <4.0.0
  • composable_architecture_core ^0.1.0
  • State classes must implement copyWith
  • Action classes must be sealed

Libraries

builder