Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/evanca-bloc/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 214% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 82% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 163% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 108% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 85% | 0% |
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.dart@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:
dartswitch (state) { case LoginInitial(): ... case LoginInProgress(): ... case LoginSuccess(:final user): ... case LoginFailure(:final message): ... }
dartenum 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.dartclass 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).dartsealed 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 |
dartBlocProvider( 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.
dartimport '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@test.com', '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@test.com', '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. |
dart// 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]));
Other measured skills in the registry, with their headline benchmark lift.