Feature SDKs
How to add a Bloc, Cubit, Riverpod or Provider class to a feature
Free Reference for state_management: every variable, option and package it generates. Command: archipelago generate state_management
Add a state class to an existing feature, choose between cubit, bloc, riverpod and provider, and connect a use case to it.
What you'll learn
- Adding a state class to an existing feature
- Choosing between cubit, bloc, riverpod and provider
- Connecting a use case to the generated class
- Creating the state holder in the shell page, not from GetIt
Prerequisites
- An existing feature module (see Feature Module Scaffolding)
- Familiarity with one of flutter_bloc, riverpod or provider
The feature bricks (feature_monorepo_skeleton, auth_sdk, home_sdk and paywall_sdk) already generate state for you from their own stateManagement variable. Use state_management to add state for another use case, or after you generated a feature with stateManagement: none.
Step 1: Generate State Management
archipelago generate state_management \
--output features/auth/auth_impl/lib/src/presentation/loginThe brick writes a flat folder. The --output flag decides where the files land. A wrong path drops the files in the wrong place with no check.
The prompts are:
- stateManagement:
cubit(default),bloc,riverpodorprovider - featureName: for example
auth - usecaseName: for example
login. Leave it empty for a blank skeleton. - outputPath: a name that feeds the printed wiring instructions
When usecaseName is set, the class is named after it. Otherwise the feature name is used.
Step 2: Non-Interactive Config
{
"stateManagement": "cubit",
"featureName": "auth",
"usecaseName": "login",
"outputPath": "login"
}archipelago generate state_management \
--config state_config.json \
--output features/auth/auth_impl/lib/src/presentation/loginStep 3: Know the Generated Files
The hook keeps only the variant you chose:
| Variant | Folder | Files |
|---|---|---|
| cubit | cubit/ | login_cubit.dart, login_state.dart, login_cubit_test.dart |
| bloc | bloc/ | login_bloc.dart, login_event.dart, login_state.dart, login_bloc_test.dart |
| riverpod | notifiers/ | login_notifier.dart, login_state.dart, login_notifier_test.dart |
| provider | notifiers/ | login_notifier.dart, login_notifier_test.dart |
cubit
The cubit takes the use case by constructor and has an execute() method:
class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._loginUseCase) : super(const LoginState.initial());
Future<void> execute() async {
emit(const LoginState.loading());
try {
await _loginUseCase();
emit(const LoginState.loaded());
} catch (e) {
emit(LoginState.error(e.toString()));
}
}
}The state has initial, loading, loaded and error variants.
bloc
The event file uses freezed, so it is a part file:
part 'login_event.freezed.dart';
@freezed
sealed class LoginEvent with _$LoginEvent {
const factory LoginEvent.started() = _Started;
const factory LoginEvent.refreshed() = _Refreshed;
}Run build_runner after you generate a bloc.
riverpod
The notifier uses the @riverpod code-generation style. It needs riverpod_generator and a build_runner pass:
@riverpod
class LoginNotifier extends _$LoginNotifier {
@override
LoginState build() => const LoginState.initial();
}provider
The notifier extends ChangeNotifier.
Step 4: Standalone or Use Case-Connected
With an empty usecaseName, the class has no injected dependency. Use it for pure UI state such as a tab index. With a usecaseName, the class takes the use case by constructor and calls it.
Step 5: Wire It into the Feature
The hook prints the wiring steps for your variant. In a feature with a container, follow this pattern:
- Register the use case in the feature's local DI with
@injectable. - In the feature shell page, create the state holder from the lease and provide it above the nested router:
BlocProvider<LoginCubit>(
create: (_) => LoginCubit(_lease.get<LoginUseCase>()),
child: const AutoRouter(),
)- Add the barrel export the hook prints, usually in
presentation.dart. - For riverpod, run
dart run build_runner build --delete-conflicting-outputs.
Pages read the holder from the widget tree and never read GetIt. The no_service_locator_* lints flag a page that does.
Step 6: Consume the State
BlocBuilder (cubit and bloc)
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) => switch (state) {
LoginInitial() => const LoginForm(),
LoginLoading() => const CircularProgressIndicator(),
LoginLoaded() => const LoginSuccessView(),
LoginError(:final message) => LoginErrorView(message: message),
},
)Use the variant class names the generated state file declares.
ConsumerWidget (riverpod)
final state = ref.watch(loginNotifierProvider);ChangeNotifierProvider (provider)
final notifier = context.watch<LoginNotifier>();Derived Flags
The hooks derive isBloc, isCubit, usesBloc (bloc or cubit), isRiverpod and isProvider from stateManagement. They are never brick variables. Templates use them for conditional files.
Riverpod Compatibility
A riverpod project pins auto_route_generator: ^10.4.0 for analyzer compatibility with riverpod_generator.
Common Patterns
| Scenario | Variant |
|---|---|
| Simple UI state (tabs, toggles) | cubit with no use case |
| Domain feature with clear events | bloc |
| State shared across features | riverpod |
| Gradual adoption in an existing app | provider |
Next Steps
- Feature containers show how use cases reach the state holder
- Feature module scaffolding if you have not generated the feature yet