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
- An existing feature module (see Feature Module Scaffolding)
- Familiarity with at least one of: flutter_bloc, riverpod, or provider
Step 1: Generate State Management
From your monorepo root, run:
mason make state_management --stateManagement cubit --featureName authOr using the Archipelago CLI:
archipelago generate state_managementAnswer the prompts:
- stateManagement —
cubit/bloc/riverpod/provider - featureName —
auth(snake_case, used to name the generated files) - usecaseName —
login(optional, connects a use case to the generated class) - outputPath — path to write files (optional, defaults to current directory)
Step 2: Non-Interactive Config
mason make state_management \
--stateManagement cubit \
--featureName auth \
--usecaseName login \
--outputPath features/auth_impl/lib/src/presentationStep 3: Generated Files by Variant
cubit
auth_cubit.dart
auth_state.dart
test/auth_cubit_test.dartauth_cubit.dart holds the AuthCubit class extending Cubit<AuthState>. When usecaseName is provided it receives the use case via constructor injection:
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.dartauth_event.dart contains a sealed AuthEvent class. auth_bloc.dart holds the AuthBloc extending Bloc<AuthEvent, AuthState> with event handlers pre-wired:
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.dartauth_notifier.dart contains an AuthNotifier class extending AsyncNotifier<AuthState> (or Notifier<AuthState> for synchronous state) with a generated provider constant:
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.dartauth_notifier.dart contains an AuthNotifier extending ChangeNotifier:
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):
mason make state_management --stateManagement cubit --featureName counterWith 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):
mason make state_management --stateManagement cubit --featureName auth --usecaseName loginStep 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:
BlocProvider<AuthCubit>(
create: (context) => getIt<AuthCubit>(),
child: const AuthShellPage(),
)Consume it in a page:
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)
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)
ChangeNotifierProvider<AuthNotifier>(
create: (_) => getIt<AuthNotifier>(),
child: const AuthShellPage(),
)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 variable | Condition |
|---|---|
isBloc | stateManagement == bloc |
isCubit | stateManagement == cubit |
usesBloc | isBloc || isCubit (both use flutter_bloc package) |
isRiverpod | stateManagement == riverpod |
isProvider | stateManagement == 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:
// 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
| Scenario | Recommended variant |
|---|---|
| Simple UI state (tabs, toggles) | cubit (no use case) |
| Domain-driven feature with clear events | bloc |
| Shared global state across features | riverpod |
| Incremental adoption into an existing app | provider |
Next Steps
- Register DI to inject use cases into your generated class
- Feature module scaffolding if you haven't generated the feature yet