Flutter Block Listener
Block Pattern

Part 7 – Flutter BLoC Tutorial: BlocBuilder, BlocListener & BlocConsumer Explained

Flutter BLoC: BlocBuilder, BlocListener & BlocConsumer

📘 Introduction

Flutter Block Listener

In Flutter app development, effective state management is crucial. The BLoC (Business Logic Component) pattern offers a reactive, stream-based way to manage state by separating business logic from the UI.

Key Widgets in the BLoC Pattern:

  • BlocBuilder – Rebuilds parts of the UI in response to new states emitted by a BLoC or Cubit. It’s used when you want the UI to reactively update based on state changes.

  • BlocListener – Listens to state changes and performs side effects such as showing snackbars, dialogs, or navigating to different screens. It does not rebuild the UI.

  • BlocConsumer – Combines BlocBuilder and BlocListener. It lets you both rebuild the UI and perform side effects in a single widget, useful when both are needed in the same context.


MultiBlocProvider – Managing Multiple BLoCs

When your app uses multiple BLoCs or Cubits, wrapping each one in its own BlocProvider becomes messy. Instead, use MultiBlocProvider, which groups multiple BlocProvider widgets together in a clean and scalable way.

This tutorial walks you through examples for each widget: a counter, an auth flow, and a shopping cart.

📂 Project Structure & Code

lib/main.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cubits/counter_cubit.dart';
import 'blocs/auth_bloc.dart';
import 'blocs/cart_bloc.dart';
import 'screens/home_page.dart';

void main() {
  runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider(create: (_) => CounterCubit()),
        BlocProvider(create: (_) => AuthBloc()),
        BlocProvider(create: (_) => CartBloc()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter BLoC Demo',
      home: HomePage(),
      routes: {
        '/home': (_) => HomePage(),
      },
    );
  }
}

lib/cubits/counter_cubit.dart


import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit {
  CounterCubit() : super(0);

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

lib/blocs/auth_bloc.dart


import 'package:flutter_bloc/flutter_bloc.dart';

abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthSuccess extends AuthState {}
class AuthFailure extends AuthState {}

class AuthBloc extends Cubit {
  AuthBloc() : super(AuthInitial());

  void login(String username, String password) {
    if (username == 'admin' && password == '123') {
      emit(AuthSuccess());
    } else {
      emit(AuthFailure());
    }
  }
}

lib/blocs/cart_bloc.dart


import 'package:flutter_bloc/flutter_bloc.dart';

// --- States ---
abstract class CartState {}

class CartLoading extends CartState {}

class CartLoaded extends CartState {
  final List items;
  CartLoaded(this.items);
}

class CheckoutSuccess extends CartState {}

// --- BLoC ---
class CartBloc extends Cubit {
  List _items = [];

  CartBloc() : super(CartLoading()) {
    loadCart();
  }

  void loadCart() async {
    await Future.delayed(Duration(seconds: 1));
    _items = ['Item 1', 'Item 2'];
    emit(CartLoaded(List.from(_items)));
  }

  void addItem(String item) {
    _items.add(item);
    emit(CartLoaded(List.from(_items)));
  }

  void checkout() {
    _items.clear();
    emit(CheckoutSuccess());
  }
}

lib/screens/home_page.dart


import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubits/counter_cubit.dart';
import '../blocs/auth_bloc.dart';
import '../blocs/cart_bloc.dart';
import 'login_form.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BLoC Demo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: SingleChildScrollView(
          // Added in case the content overflows
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('BlocBuilder (Counter):'),
              BlocBuilder<CounterCubit, int>(
                builder: (context, count) {
                  return Text('Count: $count', style: TextStyle(fontSize: 24));
                },
              ),
              Row(
                children: [
                  ElevatedButton(
                    onPressed: () => context.read().increment(),
                    child: Text('+'),
                  ),
                  SizedBox(width: 8),
                  ElevatedButton(
                    onPressed: () => context.read().decrement(),
                    child: Text('-'),
                  ),
                ],
              ),
              SizedBox(height: 24),
              Text('BlocListener (Auth):'),
              BlocListener<AuthBloc, AuthState>(
                listener: (context, state) {
                  if (state is AuthSuccess) {
                    Navigator.pushNamed(context, '/home');
                  } else if (state is AuthFailure) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Login failed')),
                    );
                  }
                },
                child: LoginForm(),
              ),
              SizedBox(height: 24),
              Text('BlocConsumer (Cart):'),
              BlocConsumer<CartBloc, CartState>(
                listener: (context, state) {
                  if (state is CheckoutSuccess) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Order placed!')),
                    );
                  }
                },
                builder: (context, state) {
                  if (state is CartLoading) {
                    return CircularProgressIndicator();
                  } else if (state is CartLoaded) {
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('Items: ${state.items.length}'),
                        SizedBox(height: 8),
                        ElevatedButton(
                          onPressed: () {
                            final newItem = 'Item ${state.items.length + 1}';
                            context.read().addItem(newItem);
                          },
                          child: Text('Add Item'),
                        ),
                        SizedBox(height: 8),
                        ElevatedButton(
                          onPressed: () => context.read().checkout(),
                          child: Text('Checkout'),
                        ),
                      ],
                    );
                  } else {
                    return Text('Cart is empty');
                  }
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

lib/screens/login_form.dart


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

class LoginForm extends StatelessWidget {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _usernameController,
          decoration: InputDecoration(labelText: 'Username'),
        ),
        TextField(
          controller: _passwordController,
          decoration: InputDecoration(labelText: 'Password'),
          obscureText: true,
        ),
        ElevatedButton(
          onPressed: () {
            context.read().login(
              _usernameController.text,
              _passwordController.text,
            );
          },
          child: Text('Login'),
        ),
      ],
    );
  }
}

🧠 Summary

BlocBuilder is used for rebuilding the UI based on new states. BlocListener is useful for handling side effects without triggering rebuilds. BlocConsumer gives you both capabilities — it’s a combination of builder and listener.

Leave a Reply

Your email address will not be published. Required fields are marked *