All Guides
    State Management
    State Management
    BLoC
    Architecture
    Advanced

    BLoC Pattern Explained

    Published April 2026

    Introduction

    As applications scale and teams expand, maintaining clean code boundaries becomes a priority. In large enterprise-scale apps, you will face complex business logic rules, dynamic API configurations, and multi-layered data pipelines. In these environments, mixing UI layouts with logical state routines leads to spaghetti code that is difficult to scale, test, or modify.

    To address this, Google introduced the BLoC (Business Logic Component) pattern. BLoC is an architectural pattern designed to separate your business logic from the user interface.

    Instead of widgets mutating states directly, the UI and logic communicate through a strict stream of Events and States. The UI sends events to the BLoC, the BLoC processes those events (e.g., executing network calls or fetching database records), and emits new states back to the UI. In this guide, you will learn the theory of event-driven streams, compare BLoC and Cubit, and build a complete user authentication manager.


    Prerequisites

    Before implementing the BLoC pattern, ensure you have:

    • Read our Managing State with Provider guide.
    • Read our Riverpod Fundamentals guide.
    • A solid grasp of Dart streams and reactive programming.
    • Added the flutter_bloc package to your project's pubspec.yaml dependencies:
      dependencies:
        flutter_bloc: ^8.1.3
      

    Core Explanation: Events, States, & Streams

    The BLoC architecture is based on a unidirectional data flow loop.

    ┌──────────────┐             Event (Input)             ┌──────────────┐
    │              ├──────────────────────────────────────►│              │
    │  UI Widget   │                                       │  BLoC Class  │
    │  (Screen)    │◄──────────────────────────────────────┤ (State logic)│
    │              │             State (Output)            └──────────────┘
    └──────────────┘
    

    1. Events (UI Inputs)

    Events are inputs sent from the user interface to the BLoC (e.g., clicking a login button, entering text, or pulling to refresh). Events tell the BLoC what actions occurred in the UI.

    2. States (UI Outputs)

    States represent the output of the BLoC and configure what the UI should display (e.g., showing a loading spinner, an error dialog, or a populated list). The UI listens to these state changes and redraws itself accordingly.

    3. Cubit vs. BLoC

    The flutter_bloc package provides two classes to manage state:

    • Cubit: A simplified version of BLoC. Instead of receiving events, a Cubit uses standard methods to emit new states. It is ideal for managing simple, localized state logic.
    • Bloc: The full event-driven implementation. It relies on streams to map events to states. This makes it ideal for complex logic, auditing user actions, or building reactive data pipelines.

    4. Integration Widgets

    • BlocProvider: Injects a BLoC instance into the widget tree.
    • BlocBuilder: Listens to state changes and rebuilds the UI.
    • BlocListener: Listens to state changes and triggers one-time side effects (like showing snackbars or navigating) without rebuilding the UI.

    Practical Example: User Authentication Flow

    Let's build a clean, compiler-ready User Authentication flow using the standard flutter_bloc pattern.

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'BLoC Auth Planner',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          // 1. Injects AuthBloc into the widget tree context
          home: BlocProvider(
            create: (context) => AuthBloc(),
            child: const AuthScreen(),
          ),
        );
      }
    }
    
    // ==========================================
    // EVENTS DEFINITION (Input States)
    // ==========================================
    abstract class AuthEvent {}
    
    class LoginRequested extends AuthEvent {
      final String username;
      const LoginRequested(this.username);
    }
    
    class LogoutRequested extends AuthEvent {}
    
    // ==========================================
    // STATES DEFINITION (Output States)
    // ==========================================
    abstract class AuthState {}
    
    class AuthInitial extends AuthState {}
    
    class AuthLoading extends AuthState {}
    
    class AuthAuthenticated extends AuthState {
      final String username;
      AuthAuthenticated(this.username);
    }
    
    class AuthUnauthenticated extends AuthState {}
    
    // ==========================================
    // BLOC LOGIC CONTROLLER
    // ==========================================
    class AuthBloc extends Bloc<AuthEvent, AuthState> {
      // Specify the initial state in the super constructor
      AuthBloc() : super(AuthInitial()) {
        // 2. Register event handlers
        on<LoginRequested>(_onLoginRequested);
        on<LogoutRequested>(_onLogoutRequested);
      }
    
      Future<void> _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async {
        emit(AuthLoading()); // Update state to show a loading spinner
        
        // Simulate a network login request delay
        await Future.delayed(const Duration(seconds: 2));
    
        if (event.username.trim().isEmpty) {
          emit(AuthUnauthenticated());
        } else {
          emit(AuthAuthenticated(event.username));
        }
      }
    
      void _onLogoutRequested(LogoutRequested event, Emitter<AuthState> emit) {
        emit(AuthUnauthenticated());
      }
    }
    
    // ==========================================
    // UI SCREEN (BlocBuilder & BlocListener)
    // ==========================================
    class AuthScreen extends StatelessWidget {
      const AuthScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        final textController = TextEditingController();
    
        return Scaffold(
          appBar: AppBar(
            title: const Text('BLoC Security Directory'),
            centerTitle: true,
          ),
          // 3. BlocListener handles one-time side effects like showing snackbars
          body: BlocListener<AuthBloc, AuthState>(
            listener: (context, state) {
              if (state is AuthAuthenticated) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Welcome back, ${state.username}!')),
                );
              }
            },
            // 4. BlocBuilder handles UI changes based on state
            child: BlocBuilder<AuthBloc, AuthState>(
              builder: (context, state) {
                // Case A: Loading Spinner State
                if (state is AuthLoading) {
                  return const Center(child: CircularProgressIndicator());
                }
    
                // Case B: Logged-in State
                if (state is AuthAuthenticated) {
                  return Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          'Signed In as: ${state.username}',
                          style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 16.0),
                        ElevatedButton(
                          onPressed: () {
                            // Send logout event to the BLoC
                            context.read<AuthBloc>().add(LogoutRequested());
                          },
                          child: const Text('Sign Out'),
                        ),
                      ],
                    ),
                  );
                }
    
                // Case C: Initial or Logged-out State
                return Padding(
                  padding: const EdgeInsets.all(24.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Text(
                        'Access Secure Content',
                        style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 16.0),
                      TextField(
                        controller: textController,
                        decoration: const InputDecoration(
                          labelText: 'Username',
                          border: OutlineInputBorder(),
                        ),
                      ),
                      const SizedBox(height: 20.0),
                      ElevatedButton(
                        onPressed: () {
                          final name = textController.text;
                          // Send login event to the BLoC
                          context.read<AuthBloc>().add(LoginRequested(name));
                        },
                        child: const Text('Sign In'),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        );
      }
    }
    

    Common Mistakes

    1. Writing State Mutations Directly in the UI

    Directly modifying state variables inside your screen widgets bypasses the BLoC pattern, creating tight coupling and making testing difficult.

    • The Fix: Keep UI widgets presentation-only. Trigger state updates by sending events to the BLoC (e.g. context.read<AuthBloc>().add(MyEvent())).

    2. Missing Providers on Root Paths

    Attempting to read a BLoC instance on a route that is not nested under the BlocProvider will crash the application with a ProviderNotFoundException.

    • The Fix: Ensure your BlocProvider is wrapped around the root of the widget subtree that needs access to the state.

    3. Emitting Identical State References

    If you emit the exact same state instance without changes, the BLoC framework will skip the rebuild step to optimize performance. This can prevent your UI from updating when data changes.

    • The Fix: Always emit a new instance when updating state (e.g., using a copyWith method to clone model objects).

    Best Practices

    • Define Clear Events and States: Structure your BLoC files with descriptive, typed subclasses for all events and states.
    • Separate cubits and Blocs: Use Cubit for simple, self-contained UI interactions (like toggling theme switches), and reserve Bloc for complex business logic workflows.
    • Isolate Rebuild Scopes: Wrap only the specific widget that needs to update in a BlocBuilder rather than rebuilding the entire screen.

    Related Concepts

    Continue mastering advanced architecture in Flutter by exploring these guides:


    Summary

    • BLoC separates UI layouts from business logic using a unidirectional flow of Events and States.
    • Events are inputs sent from the UI to the BLoC, and States are outputs emitted back to the UI.
    • Use BlocProvider to inject your BLoC, and BlocBuilder to listen for state changes and rebuild the UI.
    • Keep widgets presentation-only and handle all data transformations inside the BLoC class.

    Next Steps

    1. Previous Guide: Riverpod Fundamentals
    2. Next Guide: Widget Testing Basics
    3. Related Guide: Managing State with Provider

    Ready to take the next step?

    Start our free Flutter course and build real apps today.

    Start Learning Now