Skip to content

Generating State Management

What you'll learn

  • Generating a state management skeleton for an existing feature module
  • Choosing between cubit, bloc, riverpod, and provider
  • Connecting a use case to the generated state layer
  • Wiring the generated classes into your widget tree

Prerequisites

Step 1: Generate State Management

From your monorepo root, run:

bash
mason make state_management --stateManagement cubit --featureName auth

Or using the Archipelago CLI:

bash
archipelago generate state_management

Answer the prompts:

  • stateManagementcubit / bloc / riverpod / provider
  • featureNameauth (snake_case, used to name the generated files)
  • usecaseNamelogin (optional, connects a use case to the generated class)
  • outputPath — path to write files (optional, defaults to current directory)

Step 2: Non-Interactive Config

bash
mason make state_management \
  --stateManagement cubit \
  --featureName auth \
  --usecaseName login \
  --outputPath features/auth_impl/lib/src/presentation

Step 3: Generated Files by Variant

cubit

auth_cubit.dart
auth_state.dart
test/auth_cubit_test.dart

auth_cubit.dart holds the AuthCubit class extending Cubit<AuthState>. When usecaseName is provided it receives the use case via constructor injection:

dart
class AuthCubit extends Cubit<AuthState> {
  AuthCubit({required LoginUseCase loginUseCase})
      : _loginUseCase = loginUseCase,
        super(const AuthState.initial());

  final LoginUseCase _loginUseCase;
}

auth_state.dart contains a sealed AuthState class with initial, loading, success, and failure variants.

bloc

auth_bloc.dart
auth_event.dart
auth_state.dart
test/auth_bloc_test.dart

auth_event.dart contains a sealed AuthEvent class. auth_bloc.dart holds the AuthBloc extending Bloc<AuthEvent, AuthState> with event handlers pre-wired:

dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({required LoginUseCase loginUseCase})
      : _loginUseCase = loginUseCase,
        super(const AuthState.initial()) {
    on<AuthLoginRequested>(_onLoginRequested);
  }

  final LoginUseCase _loginUseCase;

  Future<void> _onLoginRequested(
    AuthLoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(const AuthState.loading());
    try {
      final result = await _loginUseCase(
        LoginParams(email: event.email, password: event.password),
      );
      result.fold(
        (failure) => emit(AuthState.failure(failure.message)),
        (user) => emit(AuthState.success(user)),
      );
    } catch (e) {
      emit(AuthState.failure(e.toString()));
    }
  }
}

riverpod

auth_notifier.dart
auth_state.dart
test/auth_notifier_test.dart

auth_notifier.dart contains an AuthNotifier class extending AsyncNotifier<AuthState> (or Notifier<AuthState> for synchronous state) with a generated provider constant:

dart
final authNotifierProvider =
    AsyncNotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);

class AuthNotifier extends AsyncNotifier<AuthState> {
  @override
  FutureOr<AuthState> build() => const AuthState.initial();
}

provider

auth_notifier.dart
test/auth_notifier_test.dart

auth_notifier.dart contains an AuthNotifier extending ChangeNotifier:

dart
class AuthNotifier extends ChangeNotifier {
  AuthNotifier({this.loginUseCase});

  final LoginUseCase? loginUseCase;

  AuthState _state = const AuthState.initial();
  AuthState get state => _state;
}

Step 4: Standalone vs Use Case-Connected

The usecaseName variable controls whether the generated class receives a use case dependency.

Without usecaseName — generates a standalone state manager with no injected dependencies. Suitable for pure UI state (form validation, tab index, expanded panels):

bash
mason make state_management --stateManagement cubit --featureName counter

With usecaseName — wires the named use case into the constructor and generates a stub method body that calls it. Suitable for domain-driven state (loading data, submitting forms):

bash
mason make state_management --stateManagement cubit --featureName auth --usecaseName login

Step 5: Wiring into the Widget Tree

BlocProvider (cubit / bloc)

Register the cubit or bloc in your feature's DI module and provide it at the route level:

dart
BlocProvider<AuthCubit>(
  create: (context) => getIt<AuthCubit>(),
  child: const AuthShellPage(),
)

Consume it in a page:

dart
class AuthShellPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AuthCubit, AuthState>(
      builder: (context, state) {
        return state.when(
          initial: () => const AuthInitialView(),
          loading: () => const CircularProgressIndicator(),
          success: (data) => AuthSuccessView(data: data),
          failure: (error) => AuthErrorView(error: error),
        );
      },
    );
  }
}

ConsumerWidget (riverpod)

dart
class AuthShellPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(authNotifierProvider);
    return state.when(
      data: (data) => AuthSuccessView(data: data),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => AuthErrorView(error: e),
    );
  }
}

ChangeNotifierProvider (provider)

dart
ChangeNotifierProvider<AuthNotifier>(
  create: (_) => getIt<AuthNotifier>(),
  child: const AuthShellPage(),
)
dart
class AuthShellPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final notifier = context.watch<AuthNotifier>();
    return notifier.state.when(
      initial: () => const AuthInitialView(),
      loading: () => const CircularProgressIndicator(),
      success: (data) => AuthSuccessView(data: data),
      failure: (error) => AuthErrorView(error: error),
    );
  }
}

Derived Variables from pre_gen Hooks

When generating a full monorepo via flutter_modular_monorepo, the pre_gen.dart hook computes derived boolean flags from the stateManagement enum value. These flags are used throughout templates to conditionally include files and dependencies:

Derived variableCondition
isBlocstateManagement == bloc
isCubitstateManagement == cubit
usesBlocisBloc || isCubit (both use flutter_bloc package)
isRiverpodstateManagement == riverpod
isProviderstateManagement == provider

These flags control which files are generated and which dependencies are added to pubspec.yaml.

Bloc File Structure

Bloc event and state files are standalone (not part of). Event classes are public so they can be dispatched from any widget or test:

dart
// auth_event.dart — standalone file, no part directive
sealed class AuthEvent {
  const AuthEvent();
}

final class AuthLoginRequested extends AuthEvent {
  const AuthLoginRequested({required this.email, required this.password});
  final String email;
  final String password;
}

This pattern avoids the tight coupling of part/part of directives and makes events independently importable.

Riverpod Compatibility

When using Riverpod, the generated project pins auto_route_generator: ^10.4.0 to ensure analyzer compatibility between riverpod_generator and auto_route. This avoids analyzer version conflicts during code generation.

Common Patterns

ScenarioRecommended variant
Simple UI state (tabs, toggles)cubit (no use case)
Domain-driven feature with clear eventsbloc
Shared global state across featuresriverpod
Incremental adoption into an existing appprovider

Next Steps

Built by Banua Coder