Skip to content
Back to archipelago.banuacoder.com

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 ​

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 ​

bash
archipelago generate state_management \
  --output features/auth/auth_impl/lib/src/presentation/login

The 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, riverpod or provider
  • 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 ​

json
{
  "stateManagement": "cubit",
  "featureName": "auth",
  "usecaseName": "login",
  "outputPath": "login"
}
bash
archipelago generate state_management \
  --config state_config.json \
  --output features/auth/auth_impl/lib/src/presentation/login

Step 3: Know the Generated Files ​

The hook keeps only the variant you chose:

VariantFolderFiles
cubitcubit/login_cubit.dart, login_state.dart, login_cubit_test.dart
blocbloc/login_bloc.dart, login_event.dart, login_state.dart, login_bloc_test.dart
riverpodnotifiers/login_notifier.dart, login_state.dart, login_notifier_test.dart
providernotifiers/login_notifier.dart, login_notifier_test.dart

cubit ​

The cubit takes the use case by constructor and has an execute() method:

dart
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:

dart
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:

dart
@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:

  1. Register the use case in the feature's local DI with @injectable.
  2. In the feature shell page, create the state holder from the lease and provide it above the nested router:
dart
BlocProvider<LoginCubit>(
  create: (_) => LoginCubit(_lease.get<LoginUseCase>()),
  child: const AutoRouter(),
)
  1. Add the barrel export the hook prints, usually in presentation.dart.
  2. 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) ​

dart
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) ​

dart
final state = ref.watch(loginNotifierProvider);

ChangeNotifierProvider (provider) ​

dart
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 ​

ScenarioVariant
Simple UI state (tabs, toggles)cubit with no use case
Domain feature with clear eventsbloc
State shared across featuresriverpod
Gradual adoption in an existing appprovider

Next Steps ​