
What is BLoC in Flutter?
Flutter state management is a core concept for handling data and UI synchronization in apps. One of the most widely adopted patterns for state management in Flutter applications is BLoC (Business Logic Component). It separates business logic from UI, making your code clean, testable, and easy to maintain, even for large teams or projects. BLoC for state management is suitable for both simple and complex Flutter app development. The code becomes more user friendly, maintainable, and testable.
It will handle all the state that flows through the Flutter application. Most people use BLoC to navigate major scenarios seamlessly, and any potential issues can be tracked and resolved effectively by Flutter developers. This makes the code cleaner, more manageable, and inherently testable.
In this implementation, we are using Flutter + BLoC + REST API for Flutter state management. The application includes a search country name page with a search bar and a list of results. When a country is tapped, it navigates to a details page displaying all information related to the selected country.
How does it work?
The BLoC for state management consists of three components: UI, BLoC, and Data (Repository/REST API). The UI triggers events based on user actions and renders the view. These events are passed to the BLoC, which captures and processes them to emit new states. Finally, the Data layer (repository) fetches and stores the necessary data. Fig. block diagram below.
Using the BLoC architecture makes your application more scalable and production ready especially when you’re planning to hire Flutter app developers for complex or enterprise level applications.
Core Components of the BLoC Pattern:
- Event: An event is triggered when a user interacts with the application. This event is then sent as input to the BLoC. For example, when a user clicks a button or types into a search field on the UI, the corresponding event is generated and passed to the BLoC for processing.
- States: States provide a clean separation between business logic and UI, and they represent the current condition of the application. Simply put, states are responsible for rendering the UI based on the BLoC’s output. For example, the states can be Initial, Loading, Error, or Success.
- BLoC: The BLoC contains the main business logic or core logic of the application. It captures the events triggered by the UI through user interactions and emits the corresponding states for those events. Overall, the BLoC is responsible for managing the entire state of the application.

Fig. Block Diagram
It provides three widgets: Bloc Consumer, Bloc Provider, and Bloc Listener. Using them will make it easy and simple to integrate the BLoC implementation.
- Bloc Consumer: BlocConsumer is used for both BlocListener and BlocBuilder. It will listen to the state changes in the BLoC and update the UI elements based on the current state. It will handle both the work of the listener and the builder.
BlocListener: Only for UI actions that are triggered with state changes.
BlocBuilder: Used to perform actions if the state makes some changes (primarily for rebuilding UI).
Syntax :
BlocConsumer<BlocType, StateType>(
builder: (BuildContext context, StateType state) {
// UI elements based on the current state
return Widget();
},
listener: (BuildContext context, StateType state) {
// Actions to perform when the state changes
},
)
Code language: JavaScript (javascript)
- Bloc Provider: It’s a Flutter widget that provides an instance of BLoC or Cubit.
Create: It will create an instance of cubit or bloc.
Child: The widget subtree that can access the provided BLoC.
Also, if the same, we can use multiple providers. So, for that, if there are multiple providers, it uses MultiBlocProvider
Syntax :
BlocProvider<MyBloc, MyState>(
create: (context) => MyBloc(),
child: MyApp(),
);
Code language: JavaScript (javascript)
- Bloc Listener: BlocListener only listens to the states emitted by the BLoC. It does not rebuild the UI but is used solely to handle side effects in response to state changes, such as showing a snackbar, dialog, or performing navigation
Syntax :
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: const Widget(),
);
Code language: JavaScript (javascript)

Advantages of BLoC :
Separation of Business Logic
It’s a clear separation of business logic and UI from each other. Due to this, the code will look very separate from each other, code will be maintainable and easy to understand.
Testable Outcome
We can easily test the code that is added in the business logic without involving the UI.
Handle State Management Effectively
To track state management with BLoC, it is easy and effective. As compared to other state management dependencies, it has better handling of state management.
Code is Reusable and easily understood
Due to the code being written independently of each other, other people can easily use it from any screen, widget, or class, with no duplication of code. It will help new developers structure or architecture easily. So the changes or maintenance of a project takes less time.
Scalable and Low Maintenance
In large scale applications, there are often very complex operations that are not easily scalable within the system. With BLoC, it becomes easier to manage and maintain the code across the application.
Real Time Update
Due to emitted states and events, real time updates to the UI happen easily and quickly. The screen UI reflects changes instantly, which improves the user experience as well.
Large Scale Developer Uses
BLoC is used by a large number of developers. Due to that, issues are often addressed with multiple solutions by the Flutter BLoC developer community. This helps resolve problems quickly, even at critical stages of the project.
By following this approach, you can align your architecture with industry standard mobile app development services that prioritize clean code and long term maintainability.
Disadvantages of BLoC:
Mid-Level Understanding for Beginner Developers
For beginner developers, this architecture requires a mid-level understanding. It can be difficult for newcomers to grasp at first, as they need to understand the overall structure and every concept involved. Due to the separation of code, they may need to review it multiple times to fully understand how different parts communicate with each other.
More Code for Smaller Applications (BoilerPlate code)
For big and complex applications, BLoC is suitable for small applications needing small operations and multiple code classes that need to be implemented. Need to write more code to create BLoC, event, state, and main UI screen access for actual uses is different.
Unnecessary Complexity
As developers need to create more than one file, such as BLoC, State, and Event files, for every screen or feature they want to implement, this introduces additional complexity to the codebase.
If you’re looking to implement a robust testing strategy in your projects, this detailed Flutter Testing: Unit, Widget & Integration Tests Guide will help you understand how to structure and automate different types of Flutter tests effectively.
Steps to Add BLoC for State Management in Your Flutter Project
In this implementation, we are using Flutter + BLoC + REST API to demonstrate how BLoC for state management is applied in real world projects. The application includes a search country name page with a search bar and a list of results. When a country is tapped, it navigates to a details page that displays all the information related to the selected country.
Step 1: Project Setup
First, create a Flutter project, then add the flutter_bloc: ^9.1.1
package to the dependencies: section of your project’s pubspec.yaml
file. You can find the latest version and details on the BLoC Package on pub.dev, and then run the command flutter pub get to install the dependency.
Note: Please use the latest version of flutter_bloc, which helps you to get the latest update on BLoC.
We added http: ^1.4.0
dependency which helps us to call api for real time data.
dependencies:
flutter:
sdk: flutter
# flutter bloc dependency
flutter_bloc: ^9.1.1
# Api call dependency
http: ^1.4.0
Code language: CSS (css)
Step 2: API Integration Note & Model
API Reference:
All API details are taken from the official REST Countries documentation. We are going to leverage the REST Countries (https://restcountries.com/ )
API.
For Data model creation, you can refer to the QuickType website to create a Dart model class.
API endpoint:
https://restcountries.com/v3.1/name/${event.name}
where ${event.name}
is the searchable country name provided by the user input.
Please create a corresponding API model class under the new directory, the lib folder, and add a Dart file named country_model.dart
under the Model
folder, as shown in the screenshot (refer to it for naming and structure).
Kindly adhere to the project structure to maintain consistency and ensure the intended functionality is achieved.
Please follow the project folder structure as given in the snapshot below.

Step 3: main.dart
– App Entry Point & UI
After creating a Flutter project, you’ll find a main.dart file, which serves as the main UI entry point. We need to make changes to this file as outlined in the steps below.
Sets up BlocProvider and the main screen with a search input.
- BlocProvider: Provides the BLoC to its child widgets.
- BlocBuilder: Rebuilds UI when state changes.
- User types in a search bar → event is sent to BLoC
- UI reacts to states: loading spinner, country list, error
Import Statements
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_sample_app/model/country_model.dart';
import 'package:flutter_bloc_sample_app/screen/country_details_page.dart';
import 'bloc/country_bloc.dart';
import 'bloc/country_event.dart';
import 'bloc/country_state.dart';
import 'repository/country_repository.dart';
Code language: JavaScript (javascript)
These imports bring in:
flutter_bloc
: For BLoC pattern and state management.country_model.dart
: Defines the structure of country data.country_details_page.dart
: Displays detailed country info.country_bloc.dart
,country_event.dart
,country_state.dart
: Core of the BLoC layer.country_repository.dart
: Handles actual API fetching logic (decouples logic from UI and BLoC).
App Entry Point
void main() => runApp(const MyApp());
Code language: JavaScript (javascript)
This is the main function that starts your Flutter app by running the MyApp widget.
MyApp Widget – Root of the App
class MyApp extends StatelessWidget {
const MyApp({super.key});
Code language: JavaScript (javascript)
MyApp
is the root widget of your application.StatelessWidget
because it doesn’t manage any internal state.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Country Search',
home: BlocProvider(
create: (_) => CountryBloc(repository: CountryRepository()),
child: const CountryPage(),
),
);
}
}
Code language: JavaScript (javascript)
MaterialApp
: Sets up your app’s basic structure and theming.debugShowCheckedModeBanner
: false: Hides the debug label on the top right.BlocProvider
: Provides the CountryBloc to the widget tree.CountryBloc(repository: CountryRepository())
: Injects the CountryRepository into the BLoC to fetch data.child
: Sets CountryPage as the home screen
CountryPage – Main Screen UI
class CountryPage extends StatefulWidget {
const CountryPage({super.key});
@override
State<CountryPage> createState() => _CountryPageState();
}
Code language: JavaScript (javascript)
- This is the main screen widget with a search bar and list of countries.
StatefulWidget
because it manages aTextEditingController
.
Controller Setup
final TextEditingController controller = TextEditingController();
Code language: PHP (php)
- Used to read input from the search bar.
UI Layout (build method)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search Country')),
Code language: JavaScript (javascript)
Scaffold
: Provides page structure with anAppBar
, body, etcAppBar
: Displays the screen title.
Search TextField UI
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.grey, width: 1),
),
child: TextField(
controller: controller,
onChanged: (String value) {
context.read<CountryBloc>().add(SearchCountry(value));
},
decoration: InputDecoration(
contentPadding:
EdgeInsets.symmetric(horizontal: 10, vertical: 12),
border: InputBorder.none,
hintText: 'Search country name',
suffixIcon: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
BlocProvider.of<CountryBloc>(context)
.add(SearchCountry(controller.text));
},
),
),
),
),
Code language: JavaScript (javascript)
- A TextField inside a styled container.
onChanged
: Sends aSearchCountry
event to BLoC every time the user types.suffixIcon
: Search icon button that also sends the search event manually.
BlocBuilder – List Display Based on State
const SizedBox(height: 16),
Expanded(
child: BlocBuilder<CountryBloc, CountryState>(
builder: (context, state) {
if (state is CountryLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is CountryLoaded) {
return ListView.builder(
itemCount: state.countries.length,
itemBuilder: (context, index) {
final country = state.countries[index];
Code language: PHP (php)
BlocBuilder
: Listens to the BLoC state and rebuilds the UI.CountryLoading
: Shows spinner.CountryLoaded
: Shows a list of countries.ListView.builder
: Dynamically builds country cards.
Each Country List Item
return GestureDetector(
onTap: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CountryDetailsPage(country: country),
),
);
},
child: Card(
child: ListTile(
title: Text(country.name?.official ?? "NA"),
subtitle: getCountryDetails(country)),
),
);
},
);
Code language: JavaScript (javascript)
- Tapping on a country navigates to the
CountryDetailsPage
with selected country data. - Each card displays the country’s official name and a summary.
getCountryDetails Helper Method
Widget getCountryDetails(CountryModel country) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('Capital: ${country.capital}'),
Text('Region: ${country.region}'),
Text('Subregion: ${country.subregion}'),
Text('Population: ${country.population}'),
],
);
}
Code language: JavaScript (javascript)
- Formats and displays a few key details for each country card.
Dispose Controller
@override
@override
void dispose() {
controller.dispose();
super.dispose();
}
Code language: CSS (css)
- Frees up memory by disposing of the TextEditingController when widget is removed.
Step 4: country_event.dart
– Implement event
Please create a corresponding country_event
class under the new directory in the lib->bloc
folder and add a Dart file name as country_event.dart
under the bloc
folder. Add the below mentioned code block.
abstract class CountryEvent {}
Code language: PHP (php)
What this line does:
- This defines an abstract class named
CountryEvent
. - It acts as a base class for all types of events related to the country search feature.
- You cannot create an object directly from an abstract class.
- This is useful in BLoC pattern to group related events.
class SearchCountry extends CountryEvent {
final String name;
SearchCountry(this.name);
}
Code language: JavaScript (javascript)
Explanation:
- SearchCountry is a concrete class that extends (inherits) from CountryEvent.
- This class represents a specific event: when the user searches for a country.
- final String name; → This holds the name of the country entered by the user.
- SearchCountry(this.name); → A constructor that assigns the given name to the variable.
CountryEvent
—Base class for all country related events
SearchCountry
–Specific event when user enters a country name
name
–Holds the user’s search input
Step 5: country_state.dart
– App State Definition
Please create a corresponding country_state
class under new directory the lib->bloc
folder and add a dart file name as country_state.dart
under the bloc
folder. Add the below mentioned code block.
import 'package:country_search_bloc/model/country_model.dart';
abstract class CountryState {}
class CountryInitial extends CountryState {}
class CountryLoading extends CountryState {}
class CountryLoaded extends CountryState {
final List<CountryModel> countries;
CountryLoaded(this.countries);
}
class CountryError extends CountryState {
final String message;
CountryError(this.message);
}
Code language: JavaScript (javascript)
This file defines different states used in the BLoC for country search.
- CountryInitial is the default state when the app loads.
- CountryLoading is used while the API call is in progress.
- CountryLoaded holds a list of countries when data is successfully fetched.
- CountryError shows an error message when something goes wrong during the fetch.
Step 6: country_bloc.dart
– Main BLoC Logic
Please create a corresponding country_bloc
class under new directory the lib->bloc
folder and add a dart file name as country_bloc.dart
under the bloc
folder. Add the below mentioned code block.
As all setup done just need to move toward main logic that actual bloc.
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_sample_app/repository/country_repository.dart';
import 'country_event.dart';
import 'country_state.dart';
class CountryBloc extends Bloc<CountryEvent, CountryState>
final CountryRepository _repository;
Timer? _debounceTimer;
CountryBloc({required CountryRepository repository})
: _repository = repository,
super(CountryInitial()) {
on<SearchCountry>(_onSearchCountry);
}
Future<void> _onSearchCountry(SearchCountry event, Emitter<CountryState> emit) async {
_debounceTimer?.cancel();
final completer = Completer();
_debounceTimer = Timer(const Duration(milliseconds: 500), () async {
emit(CountryLoading());
try {
final countries = await _repository.searchCountry(event.name);
emit(CountryLoaded(countries));
} catch (e) {
emit(CountryError(e.toString()));
} finally {
completer.complete();
}
});
await completer.future;
}
@override
Future<void> close() {
_debounceTimer?.cancel();
return super.close();
}
}
Code language: JavaScript (javascript)
Imports:
- dart:async: For managing asynchronous tasks and debouncing via Timer.
- flutter_bloc: To use the Bloc class from the BLoC library.
- country_repository.dart: Handles actual API fetching logic (data separation).
- country_event.dart & country_state.dart: Defines input events and output states for the BLoC.
Class Declaration
class CountryBloc extends Bloc<CountryEvent, CountryState> {
final CountryRepository _repository;
Timer? _debounceTimer;
Code language: PHP (php)
CountryBloc
listens for CountryEvents and emits CountryStates.- It uses a
CountryRepository
instance (_repository) to separate business logic from data access. - A private
_debounceTimer
is used to debounce user input (avoid calling the API on every keystroke).
Constructor
CountryBloc({required CountryRepository repository})
: _repository = repository,
super(CountryInitial()) {
on<SearchCountry>(_onSearchCountry);
}
Code language: HTML, XML (xml)
- Initializes with
CountryInitial
state. - Registers
_onSearchCountry
as the handler for SearchCountry events.
Event Handling with Debounce Logic
Future<void> _onSearchCountry(SearchCountry event, Emitter<CountryState> emit) async {
_debounceTimer?.cancel();
final completer = Completer();
_debounceTimer = Timer(const Duration(milliseconds: 500), () async {
emit(CountryLoading());
try {
final countries = await _repository.searchCountry(event.name);
emit(CountryLoaded(countries));
} catch (e) {
emit(CountryError(e.toString()));
} finally {
completer.complete();
}
});
await completer.future;
}
Code language: JavaScript (javascript)
- Cancels previous timer (if any).
- Sets a new timer for 500ms (debounce).
- After 500ms, performs API search using the repository.
- Emits corresponding BLoC states:
CountryLoading
→ before API callCountryLoaded
→ on successCountryError
→ on failure
Cleanup on Dispose
@override
Future<void> close() {
_debounceTimer?.cancel();
return super.close();
}
Code language: CSS (css)
- Cancels the timer when BLoC is disposed.
- Prevents memory leaks and dangling timers if a widget is removed or the app is closed.
Step 7: country_repository.dart
– Repository for API Logic
Please create a corresponding country_repository
class under new directory the lib->repository
folder and add a dart file name as country_repository.dart
under the repository folder.
Imports
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../model/country_model.dart';
Code language: JavaScript (javascript)
dart:convert
: To decode the JSON response from the API.http
: To make HTTP GET requests.country_model.dart
: Contains the CountryModel class used to parse each country’s data.
Class Declaration
class CountryRepository {
final String baseUrl = 'https://restcountries.com/v3.1';
Code language: JavaScript (javascript)
CountryRepository
: Class responsible for fetching data.baseUrl
: Base endpoint of the REST Countries API.
searchCountry Method
Future<List<CountryModel>> searchCountry(String name) async {
Code language: JavaScript (javascript)
- A Future function that returns a list of CountryModel objects.
- Takes name as input (the country name typed by the user).
Validation Check
if (name.trim().isEmpty) {
throw Exception('Please enter a country name.');
}
Code language: PHP (php)
If the input is empty or only spaces, it throws an exception to notify the user.
Making the API Call
final response = await http.get(Uri.<em>parse</em>('$baseUrl/name/$name'));
Code language: HTML, XML (xml)
- Makes a GET request to the endpoint:
https://restcountries.com/v3.1/name/{name}
- Example: Searching for “India” hits
https://restcountries.com/v3.1/name/India
Handling the Response
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((e) => CountryModel.fromJson(e)).toList();
} else {
throw Exception('No country found for "$name".');
}
Code language: PHP (php)
If status is 200 OK:
- Decodes the response into a list of JSON objects.
- Maps each JSON object to a
CountryModel
instance usingfromJson
. - Returns a list of
CountryModel
objects.
If status is not 200:
- Throws an exception saying the country was not found.
Error Handling
} catch (e) {
rethrow;
}
}
Code language: JavaScript (javascript)
If any unexpected error occurs (e.g., no internet, bad response), it rethrows the error to be handled by the BLoC.
Step 8: country_model.dart
– Data Model
Please create a corresponding API model class under a new directory in the lib
folder and add a Dart file named country_model.dart
under the Model
folder.
Please refer to the git repository model class.
- CountryModel: Root model class
- Nested models: Name, Flags, Currencies, etc.
- fromJson / toJson methods
Step 9: country_details_page.dart
– Country Detail Screen
Please create a corresponding country_details_page.dart
under new directory the lib->screen
folder.
This screen shows detailed information about a country when the user taps on a country from the list. The screen receives a CountryModel
object (data of one country) from the previous screen. Shows the official country name as the title.Displays the country’s flag using the image URL (flags.png
) from the API.Uses a custom method getCommonWidget()
to display country info in two-column rows. Each row shows two labels and values like
- Official Name & Common Name
- Capital & Alternative Names
- Region & Subregion
- And more…
Key Concepts in the App :
- BLoC: Business Logic Component – Manages state based on events
- Event: Trigger from UI, like typing a country name
- State: Represents UI changes like loading or error
- Debounce: Prevents API calls on every keystroke
- Model: Maps JSON to Dart class
- BlocBuilder: Rebuilds the widget tree based on state
- BlocProvider: Injects the BLoC into the widget tree
App working like →User types a country name → SearchCountry event →BLoC captures event and fetches data from REST API →Emits Loading state, then Loaded (or Error) →UI displays country list → Tap shows details screen
Please refer to the following screenshot.

Summing It Up
We hope you found this step-by-step Flutter state management tutorial helpful in understanding how to integrate BLoC for state management into your Flutter application.
To download the full source code for the sample app, click here.

Author's Bio:

Mahesh Dhoble is a Software Engineer at Mobisoft Infotech with 8.6 years of experience in developing high-quality mobile applications. He specializes in Java, Kotlin, Flutter (Dart), and Swift, with strong expertise in both native and cross-platform mobile development. Mahesh is committed to writing clean, efficient code and is passionate about continuous learning, consistently exploring new tools, technologies, and frameworks to sharpen his skills and stay updated in the fast-evolving mobile development landscape.