Flutter BLoC state management tutorial step-by-step

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.
Flutter BLoC architecture diagram for state management

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.

  1. 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)
  1. 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)
  1. 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)

Your path to 10x growth starts with a Flutter app

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.0Code 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.

Recommended folder structure for Flutter BLoC apps

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 a TextEditingController.

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 an AppBar, body, etc
  • AppBar: 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 a SearchCountry 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 call
  • CountryLoaded → on success
  • CountryError → 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)

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 using fromJson.
  • 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.

Country info in two-column layout using BLoC

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.

Build your next big idea with the right Flutter tech

Author's Bio:

Mahesh Dhoble Flutter BLoC expert author
Mahesh Dhoble

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.