Flutter Bloc

Predictable state management with business logic separation

🧩 What is Bloc?

Bloc (Business Logic Component) separates presentation from business logic using streams. It follows a unidirectional data flow pattern where events trigger state changes, making apps predictable, testable, and maintainable for complex applications.


// Event triggers state change
bloc.add(IncrementEvent());
                                    

Key Concepts

📨

Events

Input to the Bloc

abstract class CounterEvent {}
class Increment extends CounterEvent {}
📊

States

Output from the Bloc

class CounterState {
  final int count;
}
⚙️

Bloc

Converts events to states

class CounterBloc extends 
  Bloc<Event, State>
🔄

Streams

Reactive data flow

Stream<State> mapEventToState(
  Event event)

🔹 Installation

Add Bloc packages to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  equatable: ^2.0.5  # For value comparison

Then run:

flutter pub get

🔹 Basic Counter Example

Create a counter using Bloc pattern:

import 'package:flutter_bloc/flutter_bloc.dart';

// 1. Define Events
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}

// 2. Define State
class CounterState {
  final int count;
  CounterState(this.count);
}

// 3. Create Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    // Handle increment event
    on<IncrementEvent>((event, emit) {
      emit(CounterState(state.count + 1));
    });
    
    // Handle decrement event
    on<DecrementEvent>((event, emit) {
      emit(CounterState(state.count - 1));
    });
  }
}

Output:

A Bloc that handles increment and decrement events and emits new counter states.

🔹 Using Bloc in UI

Connect Bloc to your Flutter widgets:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Provide Bloc to widget tree
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        // Listen to state changes
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Text(
              '${state.count}',
              style: TextStyle(fontSize: 48),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              // Add increment event
              context.read<CounterBloc>().add(IncrementEvent());
            },
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () {
              // Add decrement event
              context.read<CounterBloc>().add(DecrementEvent());
            },
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Output:

A counter app with increment and decrement buttons using Bloc pattern.

🔹 BlocBuilder vs BlocListener

Different ways to react to state changes:

// BlocBuilder: Rebuilds UI on state change
BlocBuilder<CounterBloc, CounterState>(
  builder: (context, state) {
    return Text('Count: ${state.count}');
  },
)

// BlocListener: Performs side effects (navigation, snackbar, etc.)
BlocListener<CounterBloc, CounterState>(
  listener: (context, state) {
    if (state.count == 10) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Reached 10!')),
      );
    }
  },
  child: Container(),
)

// BlocConsumer: Combines both builder and listener
BlocConsumer<CounterBloc, CounterState>(
  listener: (context, state) {
    // Side effects
    if (state.count < 0) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Negative!')),
      );
    }
  },
  builder: (context, state) {
    // UI rebuild
    return Text('${state.count}');
  },
)

Output:

Different widgets for building UI, handling side effects, or both.

🔹 Todo List Example

A more practical Bloc example:

// Events
abstract class TodoEvent {}
class AddTodo extends TodoEvent {
  final String title;
  AddTodo(this.title);
}
class RemoveTodo extends TodoEvent {
  final int index;
  RemoveTodo(this.index);
}

// State
class TodoState {
  final List<String> todos;
  TodoState(this.todos);
}

// Bloc
class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc() : super(TodoState([])) {
    on<AddTodo>((event, emit) {
      final updatedTodos = List<String>.from(state.todos)
        ..add(event.title);
      emit(TodoState(updatedTodos));
    });
    
    on<RemoveTodo>((event, emit) {
      final updatedTodos = List<String>.from(state.todos)
        ..removeAt(event.index);
      emit(TodoState(updatedTodos));
    });
  }
}

// UI
class TodoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Todo List')),
      body: BlocBuilder<TodoBloc, TodoState>(
        builder: (context, state) {
          return ListView.builder(
            itemCount: state.todos.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(state.todos[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () {
                    context.read<TodoBloc>().add(RemoveTodo(index));
                  },
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<TodoBloc>().add(AddTodo('New Task'));
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Output:

A todo list app where you can add and remove tasks using Bloc.

🔹 Cubit: Simplified Bloc

Cubit is a simpler version of Bloc without events:

import 'package:flutter_bloc/flutter_bloc.dart';

// Cubit (no events needed)
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

// Usage in UI
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: Scaffold(
        body: Center(
          child: BlocBuilder<CounterCubit, int>(
            builder: (context, count) {
              return Text('$count', style: TextStyle(fontSize: 48));
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            context.read<CounterCubit>().increment();
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

Output:

A simpler counter using Cubit instead of full Bloc pattern.

🔹 When to Use Bloc

✅ Good For:

  • Large, complex applications
  • Apps with complex business logic
  • When testability is critical
  • Teams familiar with reactive programming
  • Apps requiring clear separation of concerns

❌ Consider Alternatives For:

  • Simple apps (use setState or Provider)
  • Beginners learning Flutter
  • Rapid prototyping
  • When boilerplate is a concern

🔹 Bloc Best Practices

  • Use Cubit for simple cases: Less boilerplate than full Bloc
  • Use Equatable: For easy state comparison
  • Keep Blocs focused: One Bloc per feature
  • Test your Blocs: Easy to test without UI
  • Use BlocObserver: For logging and debugging

🧠 Test Your Knowledge

What are the three main components of Bloc pattern?