Articles

Flutter BLoC Tutorial: Build Apps with State Management

Flutter makes building mobile apps fast and beautiful, but as apps grow, keeping logic and UI separate becomes essential. That’s where BLoC (Business Logic Component) comes in, it helps us manage state cleanly and predictably. This Flutter BLoC tutorial will teach you everything you need to know to start using BLoC in your projects.

In this tutorial, we’ll build a simple Counter App using the BLoC pattern in Flutter. You’ll code along, run it on your emulator, and see how events, states, and UI all connect seamlessly.

  • This course will introduce learners to the Flutter framework with interactive lessons on basic app components.
    • Beginner Friendly.
      1 hour
  • Learn how to build iOS applications with Swift and SwiftUI and publish them to Apples' App Store.
    • Includes 7 Courses
    • With Certificate
    • Beginner Friendly.
      13 hours

Understanding state management in Flutter

In app development, particularly within frameworks like Flutter, state refers to any data that dictates how our app’s user interface (UI) appears at a given moment. Think of it as the current condition or information that influences what the user sees and interacts with.

Consider a simple checklist app consisting of:

  • The text of each task
  • Whether a task’s checkbox is currently checked or unchecked
  • The overall progress of completed tasks (example: a progress bar)

All these pieces of information collectively form the app state. Now, let’s see why this state is so important.

Why is “state” important?

Flutter’s UI is built using widgets, which essentially describe how a part of the UI should look. A crucial concept in Flutter is that these widgets are generally immutable.

This means that once a widget is built, it doesn’t change on its own. If you want to update the UI for instance, when a user checks out a box, we can’t simply modify the existing widget. Instead, we need to rebuild the part of the UI that has changed to reflect the new state.

This is where state becomes critical. When a user interacts with our app, the underlying data (the state) changes. For the UI to visually respond to these changes, the widgets that depend on that data must be updated or rebuilt based on the new state.

Without a clear understanding and management of state, our app’s UI would remain static and unresponsive to user actions. Our main task is to use this state data and to manage it in a way that stays interactive for our users. We have many options to manage state and for this tutorial we’ll use BLoC to learn how state management works in practice.

BLoC state management illustration

Prerequisites for Flutter BLoC development

Before we start, make sure you’ve installed Flutter and an editor such as VS Code or Android Studio. You should also have an emulator or iOS simulator available.

Run this command to verify your setup:

flutter doctor

Picture of running Flutter doctor

If everything is set up correctly, you’ll see mostly green checkmarks (minor warnings are fine).

Creating a new Flutter BLoC project

Let’s start fresh by creating a new Flutter project:

flutter create bloc_starter
cd bloc_starter

Now, run the default app to make sure everything works:

flutter run

Run & verify

You should see the default Flutter counter app running on your emulator. Try changing the title text in lib/main.dart, save, and watch hot reload instantly update your app.

Initial flutter code snippet

Adding the BLoC dependencies

To use BLoC in Flutter, we need two packages:

  • flutter_bloc: provides widgets to connect UI and logic
  • equatable: helps compare states easily

Install them using:

flutter pub add flutter_bloc equatable

Adding dependencies in the BLoC app

Organizing your project structure

We’ll keep our project organized from the start. Inside the lib/ folder, create this structure:

lib/
bloc/
counter_bloc.dart
counter_event.dart
counter_state.dart
view/
counter_page.dart
app.dart
main.dart

Like this:

File structure of the app

Each folder has a clear role, where:

  • bloc/ folder handles logic (events, states, and BLoC itself)
  • view/ folder handles UI
  • app.dart and main.dart launch and structure the app

Implementing the counter BLoC architecture

The Counter App has just two actions: increment and decrement. Let’s define the BLoC components step by step.

Defining events

lib/bloc/counter_event.dart

import 'package:equatable/equatable.dart';
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object?> get props => [];
}
class CounterIncremented extends CounterEvent {
const CounterIncremented();
}
class CounterDecremented extends CounterEvent {
const CounterDecremented();
}

Showing where to paste the event code

Here, we’ve created:

  • An abstract base class CounterEvent which extends all events.
  • Two events: increment and decrement.

These events represent user actions each one triggers a change in our app.

Defining state

In Flutter BLoC, the state represents a snapshot of our app’s data at a given moment. And, for our counter app, the only piece of data we care about is the current count that is an integer. So the state’s job will be to hold that number safely and immutably. To implement this state let’s open the counter_state.dart and paste the following code inside it:

import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int value;
const CounterState({required this.value});
CounterState copyWith({int? value}) => CounterState(value: value ?? this.value);
@override
List<Object?> get props => [value];
}

Notice how extending Equatable lets Flutter and BLoC compare state objects by value instead of by reference. Well let’s understand this in an easy way where without Equatable, two states with the same value would still be seen as different objects causing unnecessary rebuilds. Why? Because even though both objects hold the same value, they live in different memory locations, so Dart sees them as different instances. That means if we are not using Equatable then the following code will return False.

CounterState(5) == CounterState(5) // true because of Equatable

Well with this the above code defines our app’s current state, the counter value. We keep it immutable and use copyWith() to create new states.

Defining the BLoC

The BLoC sits between your UI and your app’s state. It listens to incoming events (user actions) and emits new states (updated data).

Let’s implement this on our IDEs. Open counter_bloc.dart file and paste the following code:

import 'package:bloc/bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<CounterIncremented>((event, emit) {
emit(CounterState(value: state.value + 1));
});
on<CounterDecremented>((event, emit) {
emit(CounterState(value: state.value - 1));
});
}
}

In this code the CounterBloc listens for events and updates the state.

  • When it receives CounterIncremented, it increases the value.
  • When it receives CounterDecremented, it decreases the value.

Now our logic layer is ready. Let’s connect it to the UI.

Building the Flutter app UI

Creating the app shell

Your entire Flutter app is like a big tree. Everything you see on the screen, like a button, a piece of text, or a new page, is a widget, which you can think of as a branch or a leaf on that tree.

Now, every tree needs a strong root to anchor it and hold all those branches and leaves together.

That’s the special job of your app.dart file. It’s the place where you build that main “root” widget that supports your whole application and gets everything started. Let’s implement the coding part. Open the app.dart file and paste the following code inside it:

// lib/app.dart
import 'package:flutter/material.dart';
import 'view/counter_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_starter/bloc/counter_bloc.dart';
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'BLoC Counter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: BlocProvider(
create: (_) => CounterBloc(),
child: const CounterPage(),
),
);
}
}

The most important part here in the above code is the BlocProvider. Think of it as a manager we’re hiring for our app’s brain. The CounterBloc, this manager’s job is to create that brain and make sure any other part of the app, like the CounterPage, can easily access it to get information or send commands.

Wrapped inside is the MaterialApp. This is just the standard Flutter setup that gives our app its basic look and feel, like its title and color scheme.

Finally, the home property simply tells the MaterialApp which screen to show first when everything loads up. In this case, it’s pointing to our CounterPage.

This wraps our app with BlocProvider, making the CounterBloc available to all widgets.

Entry point

The main() function is the starting line for your entire Flutter application. Think of it like this: Your app might have hundreds of files and thousands of lines of code, but Flutter doesn’t get confused. It has one simple rule:

Always go to the main() function first to get its instructions.

Inside main(), you typically have just one job: to call the runApp() function. This is the command that tells Flutter, “Okay, I’m ready. Take this widget I’m giving you and make it the root of the entire app. Build it and display it on the screen.” Now, open the main.dart file and paste the following code:

import 'package:flutter/material.dart';
import 'app.dart';
void main() {
runApp(const CounterApp());
}

Creating the counter Page

The CounterPage is where the user interacts with the app. It shows the current counter number on the screen and provides buttons to increase or decrease it. This page connects directly to your CounterBloc, which means:

  • When the user presses a button, an event is sent to the bloc.
  • The bloc updates the state.
  • The UI rebuilds automatically with the new counter value.

Open the counter_page.dart file now and let’s start implementing the code now.

Setting up the counter page

We start by creating the structure of our page. This includes the AppBar and a placeholder Center widget for our counter display.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_starter/bloc/counter_bloc.dart';
import 'package:bloc_starter/bloc/counter_event.dart';
import 'package:bloc_starter/bloc/counter_state.dart';
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Counter')),
body: const Center(
child: Text('UI will go here'),
),
);
}
}

This sets up the screen layout where we’ll later display the counter and add buttons.

Displaying the counter value

Now, let’s display the counter’s current value using BlocBuilder.

BlocBuilder listens to the CounterBloc and rebuilds this section whenever the state changes.

body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'${state.value}',
style: Theme.of(context).textTheme.displayMedium,
);
},
),
),

Every time the bloc emits a new state, the text automatically updates with the latest counter value.

Adding the buttons (increment & decrement)

Let’s add two floating buttons one to increase and one to decrease the counter. Each button sends an event to the bloc when pressed.

floatingActionButton: Padding(
padding: const EdgeInsets.only(left: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'decrement',
onPressed: () =>
context.read<CounterBloc>().add(const CounterDecremented()),
child: const Icon(Icons.remove),
),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: 'increment',
onPressed: () =>
context.read<CounterBloc>().add(const CounterIncremented()),
child: const Icon(Icons.add),
),
],
),
),

Each time a button is pressed, an event is sent to the bloc, which updates the state and triggers a rebuild.

Run & verify

Run the app again:

flutter run

You’ll see the counter update when you press + or –.

If you don’t see changes, use hot reload (lightning icon) or hot restart.

Step 6: Adding one-time side effects

Let’s show a SnackBar every time the counter hits a multiple of 10.

In CounterPage, wrap the body with BlocListener:

body: BlocListener<CounterBloc, CounterState>(
listenWhen: (prev, curr) => prev.value != curr.value,
listener: (context, state) {
if (state.value != 0 && state.value % 10 == 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Milestone reached: ${state.value}')),
);
}
},
child: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'${state.value}',
style: Theme.of(context).textTheme.displayMedium,
);
},
),
),
),

With this we are done with the counter file and our last step will be to test out app.

Run & verify

Go to terminal and run the following command to test the app:

flutter run

This is how the app will look like:

Flutter BLoC app showcase

Troubleshooting

  • App doesn’t update: Try hot restart and save all files.
  • Dependency errors: Run flutter pub get again.
  • iOS build fails: Run cd ios && pod install (macOS only).

What’s next

You’ve now built a fully functional BLoC app with clean architecture! Here are a few ideas for you to extend it:

  • Add a Reset button.
  • Add a snackbar to this app where you can notify the users on hitting a specific number let’s say “10”.
  • Track history of past counts.

Conclusion

Congratulations! You’ve successfully built your first Flutter app using the BLoC pattern. This is a huge step toward writing clean, scalable, and powerful applications. You now have the foundational skills to separate your app’s logic from its UI, making your code easier to manage and test.

Ready to learn more? Dive deeper into the Flutter ecosystem with these resources:

Explore the fundamentals with our free Intro to Flutter course.

Master the building blocks of Flutter UI with our comprehensive Flutter Widgets Guide.

Frequently asked questions

1. What does BLoC mean in Flutter?

BLoC stands for Business Logic Component. It’s a design pattern that separates your app’s business logic from its UI, helping you write cleaner, testable, and more maintainable Flutter code.

2. How is BLoC different from Cubit?

Both come from the same library, but Cubit is a simplified version of BLoC.

  • BLoC uses events and states (better for complex workflows).
  • Cubit only emits states directly (great for small features).

3. Why should I use BLoC instead of setState()?

setState() works well for small apps, but it can become complicated as your app expands. BLoC separates your logic from your UI, making large apps easier to debug, scale, and test.

4. Do I need flutter_bloc for BLoC to work?

Not necessarily, you can implement BLoC manually using pure Dart streams. However, the flutter_bloc package offers ready-made widgets like BlocProvider and BlocBuilder, which make it simpler and more consistent.

5. Can I use multiple BLoCs in one Flutter app?

Absolutely. Most production apps use multiple BLoCs—one per feature or screen. Each BLoC handles a specific piece of logic, and they can communicate through events or shared repositories when needed.

6. Is BLoC suitable for all Flutter apps?

Yes, but it shines in apps that require predictable state management. If you’re building prototypes or smaller projects, Cubit or Provider might be quicker. For large-scale, enterprise apps, BLoC is one of the most reliable patterns available.

Codecademy Team

'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'

Meet the full team

Learn more on Codecademy

  • This course will introduce learners to the Flutter framework with interactive lessons on basic app components.
    • Beginner Friendly.
      1 hour
  • Learn how to build iOS applications with Swift and SwiftUI and publish them to Apples' App Store.
    • Includes 7 Courses
    • With Certificate
    • Beginner Friendly.
      13 hours