Claude Code Plugins

Community-maintained marketplace

Feedback

bloc-state-management

@valeriikot/flutter-cli
0
0

Expert Bloc/Cubit state management implementation for Flutter apps. Use when implementing state management with flutter_bloc, bloc library, Cubit pattern, or BLoC pattern. Covers events, states, business logic separation, reactive programming with streams, testing, and modern Bloc 9.x+ patterns with latest features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name bloc-state-management
description Expert Bloc/Cubit state management implementation for Flutter apps. Use when implementing state management with flutter_bloc, bloc library, Cubit pattern, or BLoC pattern. Covers events, states, business logic separation, reactive programming with streams, testing, and modern Bloc 9.x+ patterns with latest features.
allowed-tools Read, Edit, Write, Grep, Glob, Bash

Bloc State Management Skill

Expert assistance for implementing state management using the Bloc/Cubit pattern in Flutter applications.

When to Use This Skill

  • Setting up Bloc/Cubit in a Flutter project
  • Creating Blocs and Cubits for state management
  • Implementing events and states
  • Managing complex business logic with Bloc pattern
  • Handling async operations and data fetching
  • Implementing form validation with Bloc
  • Testing Blocs and Cubits
  • Using Bloc observers and transformers
  • Optimizing Bloc performance
  • Migrating between Bloc versions

Setup

Dependencies

dependencies:
  flutter_bloc: ^9.0.0
  equatable: ^2.0.5  # For value equality

dev_dependencies:
  bloc_test: ^10.0.0
  mocktail: ^1.0.0

Project Setup

# Add dependencies
flutter pub add flutter_bloc equatable
flutter pub add --dev bloc_test mocktail

App Configuration

import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  // Optional: Global Bloc observer
  Bloc.observer = AppBlocObserver();

  runApp(const MyApp());
}

// Bloc Observer for debugging
class AppBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('${bloc.runtimeType} $error $stackTrace');
    super.onError(bloc, error, stackTrace);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('${bloc.runtimeType} $transition');
  }

  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('${bloc.runtimeType} $event');
  }
}

Core Concepts

Cubit vs Bloc

Feature Cubit Bloc
Complexity Simple Complex
Input Methods Events
Best for Simple state Complex business logic
Testability Easy Very structured
Boilerplate Less More
Traceability Good Excellent

Rule of thumb: Use Cubit for simple state, Bloc for complex logic with clear events.

Cubit Pattern (Simple State Management)

Basic Cubit

import 'package:flutter_bloc/flutter_bloc.dart';

// Simple state (can be primitive or class)
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
  void reset() => emit(0);
}

// In widget
BlocProvider(
  create: (context) => CounterCubit(),
  child: CounterView(),
)

Cubit with Complex State

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// State class
class TodoState extends Equatable {
  const TodoState({
    this.todos = const [],
    this.isLoading = false,
    this.error,
  });

  final List<Todo> todos;
  final bool isLoading;
  final String? error;

  TodoState copyWith({
    List<Todo>? todos,
    bool? isLoading,
    String? error,
  }) {
    return TodoState(
      todos: todos ?? this.todos,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }

  @override
  List<Object?> get props => [todos, isLoading, error];
}

// Cubit
class TodoCubit extends Cubit<TodoState> {
  TodoCubit(this._repository) : super(const TodoState());

  final TodoRepository _repository;

  Future<void> loadTodos() async {
    emit(state.copyWith(isLoading: true, error: null));

    try {
      final todos = await _repository.fetchTodos();
      emit(state.copyWith(todos: todos, isLoading: false));
    } catch (e) {
      emit(state.copyWith(
        isLoading: false,
        error: e.toString(),
      ));
    }
  }

  Future<void> addTodo(String title) async {
    try {
      final newTodo = await _repository.addTodo(title);
      emit(state.copyWith(
        todos: [...state.todos, newTodo],
      ));
    } catch (e) {
      emit(state.copyWith(error: e.toString()));
    }
  }

  void removeTodo(String id) {
    emit(state.copyWith(
      todos: state.todos.where((todo) => todo.id != id).toList(),
    ));
  }

  @override
  Future<void> close() {
    // Cleanup if needed
    return super.close();
  }
}

Bloc Pattern (Event-Driven State Management)

Complete Bloc Example

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

// Events
abstract class TodoEvent extends Equatable {
  const TodoEvent();

  @override
  List<Object?> get props => [];
}

class TodoLoadRequested extends TodoEvent {
  const TodoLoadRequested();
}

class TodoAdded extends TodoEvent {
  const TodoAdded(this.title);

  final String title;

  @override
  List<Object?> get props => [title];
}

class TodoDeleted extends TodoEvent {
  const TodoDeleted(this.id);

  final String id;

  @override
  List<Object?> get props => [id];
}

class TodoToggled extends TodoEvent {
  const TodoToggled(this.id);

  final String id;

  @override
  List<Object?> get props => [id];
}

// States
abstract class TodoState extends Equatable {
  const TodoState();

  @override
  List<Object?> get props => [];
}

class TodoInitial extends TodoState {
  const TodoInitial();
}

class TodoLoadInProgress extends TodoState {
  const TodoLoadInProgress();
}

class TodoLoadSuccess extends TodoState {
  const TodoLoadSuccess(this.todos);

  final List<Todo> todos;

  @override
  List<Object?> get props => [todos];
}

class TodoLoadFailure extends TodoState {
  const TodoLoadFailure(this.error);

  final String error;

  @override
  List<Object?> get props => [error];
}

// Bloc
class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc(this._repository) : super(const TodoInitial()) {
    on<TodoLoadRequested>(_onLoadRequested);
    on<TodoAdded>(_onAdded);
    on<TodoDeleted>(_onDeleted);
    on<TodoToggled>(_onToggled);
  }

  final TodoRepository _repository;

  Future<void> _onLoadRequested(
    TodoLoadRequested event,
    Emitter<TodoState> emit,
  ) async {
    emit(const TodoLoadInProgress());

    try {
      final todos = await _repository.fetchTodos();
      emit(TodoLoadSuccess(todos));
    } catch (e) {
      emit(TodoLoadFailure(e.toString()));
    }
  }

  Future<void> _onAdded(
    TodoAdded event,
    Emitter<TodoState> emit,
  ) async {
    if (state is TodoLoadSuccess) {
      try {
        final newTodo = await _repository.addTodo(event.title);
        final currentState = state as TodoLoadSuccess;
        emit(TodoLoadSuccess([...currentState.todos, newTodo]));
      } catch (e) {
        emit(TodoLoadFailure(e.toString()));
      }
    }
  }

  void _onDeleted(
    TodoDeleted event,
    Emitter<TodoState> emit,
  ) {
    if (state is TodoLoadSuccess) {
      final currentState = state as TodoLoadSuccess;
      emit(TodoLoadSuccess(
        currentState.todos.where((todo) => todo.id != event.id).toList(),
      ));
    }
  }

  void _onToggled(
    TodoToggled event,
    Emitter<TodoState> emit,
  ) {
    if (state is TodoLoadSuccess) {
      final currentState = state as TodoLoadSuccess;
      emit(TodoLoadSuccess(
        currentState.todos.map((todo) {
          return todo.id == event.id
              ? todo.copyWith(completed: !todo.completed)
              : todo;
        }).toList(),
      ));
    }
  }

  @override
  Future<void> close() {
    // Cleanup
    return super.close();
  }
}

Widget Integration

BlocProvider (Single Bloc)

BlocProvider(
  create: (context) => TodoCubit(
    context.read<TodoRepository>(),
  )..loadTodos(),  // Can call methods immediately
  child: const TodoView(),
)

MultiBlocProvider (Multiple Blocs)

MultiBlocProvider(
  providers: [
    BlocProvider(create: (context) => AuthBloc()),
    BlocProvider(create: (context) => TodoBloc()),
    BlocProvider(create: (context) => SettingsBloc()),
  ],
  child: const MyApp(),
)

BlocBuilder (Rebuild on State Change)

BlocBuilder<TodoCubit, TodoState>(
  builder: (context, state) {
    if (state.isLoading) {
      return const CircularProgressIndicator();
    }

    if (state.error != null) {
      return Text('Error: ${state.error}');
    }

    return ListView.builder(
      itemCount: state.todos.length,
      itemBuilder: (context, index) {
        return TodoItem(todo: state.todos[index]);
      },
    );
  },
)

// With buildWhen for optimization
BlocBuilder<CounterCubit, int>(
  buildWhen: (previous, current) => previous != current,
  builder: (context, state) => Text('$state'),
)

BlocSelector (Rebuild on Specific Property)

// Only rebuilds when name changes, not entire user state
BlocSelector<UserCubit, UserState, String>(
  selector: (state) => state.name,
  builder: (context, name) => Text(name),
)

BlocListener (Side Effects)

BlocListener<TodoBloc, TodoState>(
  listener: (context, state) {
    if (state is TodoLoadFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.error)),
      );
    }
  },
  child: const TodoView(),
)

// Listen to specific events
BlocListener<TodoBloc, TodoState>(
  listenWhen: (previous, current) {
    return previous is TodoLoadInProgress &&
           current is TodoLoadSuccess;
  },
  listener: (context, state) {
    // Show success message
  },
  child: const TodoView(),
)

BlocConsumer (Builder + Listener)

BlocConsumer<TodoBloc, TodoState>(
  listener: (context, state) {
    if (state is TodoLoadFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.error)),
      );
    }
  },
  builder: (context, state) {
    if (state is TodoLoadInProgress) {
      return const CircularProgressIndicator();
    }

    if (state is TodoLoadSuccess) {
      return TodoList(todos: state.todos);
    }

    return const SizedBox();
  },
)

Reading/Dispatching Events

// Read Cubit/Bloc instance (doesn't rebuild)
context.read<TodoCubit>().addTodo('New Todo');

// Watch state (rebuilds on change)
final state = context.watch<TodoCubit>().state;

// Select specific property
final count = context.select(
  (TodoCubit cubit) => cubit.state.todos.length,
);

// For Bloc: dispatch events
context.read<TodoBloc>().add(const TodoAdded('New Todo'));

Advanced Patterns

Bloc Transformers (Event Processing)

import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc() : super(const SearchState()) {
    // Debounce search events
    on<SearchQueryChanged>(
      _onQueryChanged,
      transformer: debounce(const Duration(milliseconds: 300)),
    );

    // Sequential processing
    on<DataLoadRequested>(
      _onDataLoadRequested,
      transformer: sequential(),
    );

    // Concurrent processing
    on<DataRefreshRequested>(
      _onDataRefreshRequested,
      transformer: concurrent(),
    );

    // Drop previous events
    on<ButtonPressed>(
      _onButtonPressed,
      transformer: droppable(),
    );

    // Restart on new event
    on<InputChanged>(
      _onInputChanged,
      transformer: restartable(),
    );
  }
}

// Custom debounce transformer
EventTransformer<E> debounce<E>(Duration duration) {
  return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

Bloc Communication

// Bloc listening to another Bloc
class DependentBloc extends Bloc<DependentEvent, DependentState> {
  DependentBloc(this._authBloc) : super(DependentInitial()) {
    _authSubscription = _authBloc.stream.listen((authState) {
      if (authState is Authenticated) {
        add(const DataLoadRequested());
      } else if (authState is Unauthenticated) {
        add(const DataCleared());
      }
    });

    on<DataLoadRequested>(_onDataLoadRequested);
    on<DataCleared>(_onDataCleared);
  }

  final AuthBloc _authBloc;
  late StreamSubscription _authSubscription;

  @override
  Future<void> close() {
    _authSubscription.cancel();
    return super.close();
  }
}

Hydrated Bloc (Persistence)

// Add dependency: hydrated_bloc: ^9.1.2

import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  HydratedBloc.storage = await HydratedStorage.build(
    storageDirectory: await getApplicationDocumentsDirectory(),
  );

  runApp(const MyApp());
}

// Hydrated Cubit
class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  int? fromJson(Map<String, dynamic> json) => json['value'] as int?;

  @override
  Map<String, dynamic>? toJson(int state) => {'value': state};
}

Testing

Cubit Testing

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockTodoRepository extends Mock implements TodoRepository {}

void main() {
  group('TodoCubit', () {
    late TodoRepository repository;

    setUp(() {
      repository = MockTodoRepository();
    });

    test('initial state is TodoState()', () {
      expect(
        TodoCubit(repository).state,
        equals(const TodoState()),
      );
    });

    blocTest<TodoCubit, TodoState>(
      'emits loading and success when loadTodos succeeds',
      build: () {
        when(() => repository.fetchTodos())
            .thenAnswer((_) async => [Todo(id: '1', title: 'Test')]);
        return TodoCubit(repository);
      },
      act: (cubit) => cubit.loadTodos(),
      expect: () => [
        const TodoState(isLoading: true),
        TodoState(
          isLoading: false,
          todos: [Todo(id: '1', title: 'Test')],
        ),
      ],
    );

    blocTest<TodoCubit, TodoState>(
      'emits loading and failure when loadTodos fails',
      build: () {
        when(() => repository.fetchTodos())
            .thenThrow(Exception('Failed'));
        return TodoCubit(repository);
      },
      act: (cubit) => cubit.loadTodos(),
      expect: () => [
        const TodoState(isLoading: true),
        const TodoState(
          isLoading: false,
          error: 'Exception: Failed',
        ),
      ],
    );
  });
}

Bloc Testing

void main() {
  group('TodoBloc', () {
    late TodoRepository repository;

    setUp(() {
      repository = MockTodoRepository();
    });

    test('initial state is TodoInitial', () {
      expect(TodoBloc(repository).state, equals(const TodoInitial()));
    });

    blocTest<TodoBloc, TodoState>(
      'emits [TodoLoadInProgress, TodoLoadSuccess] when load succeeds',
      build: () {
        when(() => repository.fetchTodos())
            .thenAnswer((_) async => [Todo(id: '1', title: 'Test')]);
        return TodoBloc(repository);
      },
      act: (bloc) => bloc.add(const TodoLoadRequested()),
      expect: () => [
        const TodoLoadInProgress(),
        TodoLoadSuccess([Todo(id: '1', title: 'Test')]),
      ],
    );

    blocTest<TodoBloc, TodoState>(
      'emits [TodoLoadInProgress, TodoLoadFailure] when load fails',
      build: () {
        when(() => repository.fetchTodos())
            .thenThrow(Exception('Failed'));
        return TodoBloc(repository);
      },
      act: (bloc) => bloc.add(const TodoLoadRequested()),
      expect: () => [
        const TodoLoadInProgress(),
        const TodoLoadFailure('Exception: Failed'),
      ],
    );

    blocTest<TodoBloc, TodoState>(
      'emits updated list when todo added',
      build: () {
        when(() => repository.fetchTodos())
            .thenAnswer((_) async => []);
        when(() => repository.addTodo(any()))
            .thenAnswer((_) async => Todo(id: '1', title: 'New'));
        return TodoBloc(repository)..add(const TodoLoadRequested());
      },
      seed: () => const TodoLoadSuccess([]),
      act: (bloc) => bloc.add(const TodoAdded('New')),
      expect: () => [
        TodoLoadSuccess([Todo(id: '1', title: 'New')]),
      ],
    );
  });
}

Widget Testing with Bloc

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mocktail/mocktail.dart';

class MockTodoBloc extends MockBloc<TodoEvent, TodoState>
    implements TodoBloc {}

void main() {
  late TodoBloc todoBloc;

  setUp(() {
    todoBloc = MockTodoBloc();
  });

  testWidgets('renders loading indicator when loading', (tester) async {
    when(() => todoBloc.state).thenReturn(const TodoLoadInProgress());

    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: todoBloc,
          child: const TodoView(),
        ),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('renders todos when loaded', (tester) async {
    when(() => todoBloc.state).thenReturn(
      TodoLoadSuccess([
        Todo(id: '1', title: 'Test Todo'),
      ]),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: todoBloc,
          child: const TodoView(),
        ),
      ),
    );

    expect(find.text('Test Todo'), findsOneWidget);
  });
}

Project Structure

lib/
├── core/
│   ├── blocs/           # Shared blocs (auth, theme, etc.)
│   └── utils/
├── features/
│   └── todos/
│       ├── data/
│       │   ├── models/
│       │   └── repositories/
│       ├── domain/
│       │   └── entities/
│       └── presentation/
│           ├── blocs/    # Feature-specific blocs/cubits
│           │   └── todo/
│           │       ├── todo_bloc.dart
│           │       ├── todo_event.dart
│           │       └── todo_state.dart
│           ├── pages/
│           └── widgets/
└── main.dart

Common Patterns

Form Validation Bloc

class LoginFormCubit extends Cubit<LoginFormState> {
  LoginFormCubit() : super(const LoginFormState());

  void emailChanged(String email) {
    final emailError = _validateEmail(email);
    emit(state.copyWith(
      email: email,
      emailError: emailError,
    ));
  }

  void passwordChanged(String password) {
    final passwordError = _validatePassword(password);
    emit(state.copyWith(
      password: password,
      passwordError: passwordError,
    ));
  }

  Future<void> submit() async {
    if (!state.isValid) return;

    emit(state.copyWith(status: FormStatus.submitting));

    try {
      await _authRepository.login(state.email, state.password);
      emit(state.copyWith(status: FormStatus.success));
    } catch (e) {
      emit(state.copyWith(
        status: FormStatus.failure,
        errorMessage: e.toString(),
      ));
    }
  }

  String? _validateEmail(String email) {
    if (email.isEmpty) return 'Email is required';
    if (!email.contains('@')) return 'Invalid email';
    return null;
  }

  String? _validatePassword(String password) {
    if (password.isEmpty) return 'Password is required';
    if (password.length < 6) return 'Password too short';
    return null;
  }
}

Pagination Bloc

class PostListBloc extends Bloc<PostListEvent, PostListState> {
  PostListBloc(this._repository) : super(const PostListState()) {
    on<PostListLoadRequested>(_onLoadRequested);
    on<PostListLoadMore>(_onLoadMore);
  }

  final PostRepository _repository;
  static const _pageSize = 20;

  Future<void> _onLoadRequested(
    PostListLoadRequested event,
    Emitter<PostListState> emit,
  ) async {
    emit(state.copyWith(status: PostListStatus.loading));

    try {
      final posts = await _repository.fetchPosts(0, _pageSize);
      emit(state.copyWith(
        status: PostListStatus.success,
        posts: posts,
        hasMore: posts.length >= _pageSize,
      ));
    } catch (e) {
      emit(state.copyWith(
        status: PostListStatus.failure,
        error: e.toString(),
      ));
    }
  }

  Future<void> _onLoadMore(
    PostListLoadMore event,
    Emitter<PostListState> emit,
  ) async {
    if (!state.hasMore || state.isLoadingMore) return;

    emit(state.copyWith(isLoadingMore: true));

    try {
      final posts = await _repository.fetchPosts(
        state.posts.length,
        _pageSize,
      );

      emit(state.copyWith(
        posts: [...state.posts, ...posts],
        hasMore: posts.length >= _pageSize,
        isLoadingMore: false,
      ));
    } catch (e) {
      emit(state.copyWith(
        isLoadingMore: false,
        error: e.toString(),
      ));
    }
  }
}

Best Practices

  • Use Cubit for simple state, Bloc for complex event-driven logic
  • Use Equatable for automatic value comparison
  • Separate events, states, and bloc into different files for clarity
  • Use sealed classes or freezed for exhaustive state matching
  • Name events in past tense (UserLoggedIn, DataLoadRequested)
  • Name states descriptively (Loading, Success, Failure)
  • Use BlocObserver for debugging and logging
  • Test blocs thoroughly with bloc_test
  • Use event transformers to control event processing
  • Avoid accessing BuildContext in Blocs (pass data through events)
  • Close blocs properly to prevent memory leaks
  • Use BlocProvider.value only for existing instances
  • Use BlocSelector to optimize rebuilds
  • Keep business logic in Blocs, not in widgets
  • Use repositories for data access, not directly in Blocs

Common Issues

Issue: BlocProvider.of() called with no matching provider

Solution: Wrap widget with BlocProvider

BlocProvider(
  create: (context) => TodoBloc(),
  child: const TodoView(),
)

Issue: Bloc is not updating UI

Solution: Ensure states are properly compared (use Equatable)

class TodoState extends Equatable {
  @override
  List<Object?> get props => [todos, isLoading];
}

Issue: Memory leak

Solution: Close blocs and cancel subscriptions

@override
Future<void> close() {
  _subscription.cancel();
  return super.close();
}

Issue: Cannot emit new states after close

Solution: Check if closed before emitting

if (!isClosed) {
  emit(newState);
}

Resources

Notes

This skill focuses on Bloc/Cubit patterns. For Flutter-specific UI patterns, use the flutter-developer skill. For alternative state management, see the riverpod-state-management skill. Consider using code generation tools like freezed for immutable state classes.