Use when creating a Cubit or Bloc, modeling state with sealed classes or status enums, wiring BlocBuilder/BlocListener/BlocProvider, writing bloc tests, or choosing between Cubit and Bloc.
npx skills add https://github.com/evanca/flutter-ai-rules --skill bloc
Design, implement, and test state management using the bloc and flutter_bloc libraries.
Use this skill when:
BlocBuilder, BlocListener, BlocConsumer, or BlocProvider in the widget tree.| Situation | Use |
|---|---|
| Simple state, no events needed | Cubit |
| Complex flows, event traceability needed | Bloc |
| Advanced event processing (debounce, throttle) | Bloc with event transformers |
Default to Cubit. Refactor to Bloc only when requirements grow.
LoginButtonPressed, UserProfileLoaded.BlocSubject + optional noun + verb.BlocSubjectStarted (e.g., AuthenticationStarted).BlocSubjectEvent.BlocSubjectState.BlocSubject + Initial | InProgress | Success | Failure.LoginInitial, LoginInProgress, LoginSuccess, LoginFailure.BlocSubjectState + BlocSubjectStatus enum (initial, loading, success, failure).switch is desired.@immutable
sealed class LoginState extends Equatable {
const LoginState();
}
final class LoginInitial extends LoginState {
@override
List<Object?> get props => [];
}
final class LoginInProgress extends LoginState {
@override
List<Object?> get props => [];
}
final class LoginSuccess extends LoginState {
const LoginSuccess(this.user);
final User user;
@override
List<Object?> get props => [user];
}
final class LoginFailure extends LoginState {
const LoginFailure(this.message);
final String message;
@override
List<Object?> get props => [message];
}
Handle all states exhaustively in the UI:
switch (state) {
case LoginInitial(): ...
case LoginInProgress(): ...
case LoginSuccess(:final user): ...
case LoginFailure(:final message): ...
}
enum LoginStatus { initial, loading, success, failure }
@immutable
class LoginState extends Equatable {
const LoginState({
this.status = LoginStatus.initial,
this.user,
this.errorMessage,
});
final LoginStatus status;
final User? user;
final String? errorMessage;
LoginState copyWith({
LoginStatus? status,
User? user,
String? errorMessage,
}) {
return LoginState(
status: status ?? this.status,
user: user ?? this.user,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, user, errorMessage];
}
Equatable and pass all relevant fields to props.List/Map properties with List.of/Map.of inside props.@immutable.class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._authRepository) : super(const LoginState());
final AuthRepository _authRepository;
Future<void> login(String email, String password) async {
emit(state.copyWith(status: LoginStatus.loading));
try {
final user = await _authRepository.login(email, password);
emit(state.copyWith(status: LoginStatus.success, user: user));
} catch (e) {
emit(state.copyWith(status: LoginStatus.failure, errorMessage: e.toString()));
}
}
}
Rules:
emit inside the Cubit/Bloc.void or Future<void> only.storage in a HydratedCubit, pass it as a named parameter: super(initialState, storage: storage).sealed class LoginEvent {}
final class LoginSubmitted extends LoginEvent {
LoginSubmitted({required this.email, required this.password});
final String email;
final String password;
}
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc(this._authRepository) : super(LoginInitial()) {
on<LoginSubmitted>(_onLoginSubmitted);
}
final AuthRepository _authRepository;
Future<void> _onLoginSubmitted(
LoginSubmitted event,
Emitter<LoginState> emit,
) async {
emit(LoginInProgress());
try {
final user = await _authRepository.login(event.email, event.password);
emit(LoginSuccess(user));
} catch (e) {
emit(LoginFailure(e.toString()));
}
}
}
Rules:
bloc.add(Event()), not custom public methods._onEventName).Three layers — each must stay in its own boundary:
Presentation → Business Logic (Cubit/Bloc) → Data (Repository → DataProvider)
Rules:
BlocListener in the UI to bridge blocs.BlocObserver in main.dart.| Widget | Use |
|---|---|
| BlocProvider | Provide a bloc to a subtree |
| MultiBlocProvider | Provide multiple blocs without nesting |
| BlocBuilder | Rebuild UI on state change |
| BlocListener | Side effects only (navigation, dialogs, snackbars) |
| MultiBlocListener | Listen to multiple blocs without nesting |
| BlocConsumer | Rebuild UI + side effects together |
| BlocSelector | Rebuild only when a selected slice of state changes |
| RepositoryProvider | Provide a repository to the widget tree |
| MultiRepositoryProvider | Provide multiple repositories without nesting |
BlocProvider(
create: (context) => LoginCubit(context.read<AuthRepository>()),
child: LoginView(),
);
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
return switch (state.status) {
LoginStatus.loading => const CircularProgressIndicator(),
LoginStatus.success => const HomeView(),
LoginStatus.failure => Text(state.errorMessage ?? 'Error'),
LoginStatus.initial => const LoginForm(),
};
},
);
BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.status == LoginStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Login failed')),
);
}
},
child: LoginForm(),
);
Rules:
context.read<T>() in callbacks (not in build).context.watch<T>() in build only when necessary; prefer BlocBuilder.context.watch or context.select at the root of build — scope with Builder.Use bloc_test package. Mock repositories with mocktail.
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
group('LoginCubit', () {
late AuthRepository authRepository;
late LoginCubit loginCubit;
setUp(() {
authRepository = MockAuthRepository();
loginCubit = LoginCubit(authRepository);
});
tearDown(() => loginCubit.close());
test('initial state should be LoginState with status initial', () {
expect(loginCubit.state, const LoginState());
});
blocTest<LoginCubit, LoginState>(
'should emit [loading, success] when login succeeds',
build: () {
when(() => authRepository.login(any(), any()))
.thenAnswer((_) async => fakeUser);
return loginCubit;
},
act: (cubit) => cubit.login('[email protected]', 'password'),
expect: () => [
const LoginState(status: LoginStatus.loading),
LoginState(status: LoginStatus.success, user: fakeUser),
],
);
blocTest<LoginCubit, LoginState>(
'should emit [loading, failure] when login throws',
build: () {
when(() => authRepository.login(any(), any()))
.thenThrow(Exception('error'));
return loginCubit;
},
act: (cubit) => cubit.login('[email protected]', 'wrong'),
expect: () => [
const LoginState(status: LoginStatus.loading),
isA<LoginState>().having((s) => s.status, 'status', LoginStatus.failure),
],
);
});
}
Rules:
tearDown(() => cubit.close()).blocTest for state emission assertions.group() named after the class under test.registerFallbackValue(MyEvent()).| Pitfall | Fix |
|---|---|
| Emitting the same state instance twice | Always create a new state object; bloc ignores duplicate emissions via ==. |
| Calling context.watch inside callbacks | Use context.read in callbacks; watch is only valid inside build. |
| Forgetting Equatable props | Add every field to props; missing fields cause silent state update bugs. |
| Mutable state fields | Keep state @immutable; use copyWith or new sealed subclass instances. |
| Business logic in widgets | Move all logic into the Cubit/Bloc; widgets only dispatch events or call methods. |
// BAD — mutating state in-place
state.items.add(newItem);
emit(state);
// GOOD — emit a new state with copied list
emit(state.copyWith(items: [...state.items, newItem]));
Use when implementing any feature or bugfix, before writing implementation code
Use when implementing any feature or bugfix, before writing implementation code - write the test first, watch it fail, write minimal code to pass; ensures tests actually verify behavior by requiring failure first
Use when creating new skills, editing existing skills, or verifying skills work before deployment - applies TDD to process documentation by testing with subagents before writing, iterating until bulletproof against rationalization
Rules for writing and reviewing tests in the Remotion repository. Use whenever adding, editing, or reviewing tests, especially for Studio UI, rendering, CLI, server, media, and cross-package changes, to prefer complete integration workflows over narrow helper tests and implementation details.
Patterns and pitfalls for the ONNX domain Attention operator (opset 23/24) CUDA implementation. Use when modifying the dispatch cascade in core/providers/cuda/llm/attention.cc, writing mask/bias CUDA kernels, debugging attention test routing, or adding features to the ONNX Attention op. NOT for contrib domain MultiHeadAttention/GroupQueryAttention.
> Translates a HuggingFace model into a prefill-only AutoDeploy custom model using reference custom ops, validates with hierarchical equivalence tests.
Translates a HuggingFace model into a prefill-only AutoDeploy custom model using reference custom ops, validates with hierarchical equivalence tests.
Generates Angular 17+ standalone components, configures advanced routing with lazy loading and guards, implements NgRx state management, applies RxJS patterns, and optimizes bundle performance. Use when building Angular 17+ applications with standalone components or signals, setting up NgRx stores, establishing RxJS reactive patterns, performance tuning, or writing Angular tests for enterprise apps.
Take evanca/bloc from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.