🚀 Quick Start (Copy‑Paste) – If you just want a working BLoC API call right now, copy the complete code from the Try Editor at the bottom of this page. It includes a full example with http, flutter_bloc, and Equatable. For production apps, read the entire guide.
📌 Summary (Featured Snippet) – BLoC API integration in Flutter helps manage REST API calls using a reactive pattern with loading, success, and error states, improving scalability and testability. This guide covers Dio, pagination, clean architecture, and production‑ready code.
In real-world Flutter apps, fetching data from APIs is unavoidable. Without proper state management, handling loading, errors, and pagination becomes messy. This guide shows how to use BLoC to build scalable, production-ready API integrations with clean architecture. Whether you need a simple Flutter REST API example or a full BLoC API tutorial, you'll find everything here.
Why Use BLoC for API Calls?
Separation of concerns – Business logic stays out of the UI.
Reactive state – UI automatically rebuilds when data arrives.
Testability – Blocs and repositories can be tested without the UI.
Error handling – Dedicated error states provide user feedback.
Loading indicators – Built‑in loading states improve UX.
For a deeper understanding of BLoC architecture, check our guide on <a href='/tech/flutter/bloc-clean-architecture'>BLoC Clean Architecture</a> and <a href='/tech/flutter/bloc-error-handling'>BLoC Error Handling</a>.
Project Setup
Add the necessary dependencies to your pubspec.yaml:
YAMLRead-only
1
dependencies:flutter:sdk: flutter
flutter_bloc:^8.1.5equatable:^2.0.5http:^1.2.0 # or dio:^5.4.0get_it:^7.6.7 # optional forDIdev_dependencies:bloc_test:^9.1.5mockito:^5.4.3
Run flutter pub get.
Data Model
Create a model class for the data you're fetching. We'll use JSONPlaceholder's posts endpoint as an example.
DARTRead-only
1
import'package:equatable/equatable.dart';classPostextendsEquatable{
final int id;
final int userId;
final String title;
final String body;constPost({
required this.id,
required this.userId,
required this.title,
required this.body,});
factory Post.fromJson(Map<String, dynamic> json){returnPost(id: json['id'],userId: json['userId'],title: json['title'],body: json['body'],);}
@override
List<Object?>getprops=>[id, userId, title, body];}
States
Define the states your bloc can be in. For a typical fetch, you'll have loading, success, and error states.
DARTRead-only
1
import'package:equatable/equatable.dart';import'../../models/post.dart';
abstract classPostStateextendsEquatable{constPostState();
@override
List<Object?>getprops=>[];}classPostInitialextendsPostState{}classPostLoadingextendsPostState{}classPostLoadedextendsPostState{
final List<Post> posts;constPostLoaded(this.posts);
@override
List<Object?>getprops=>[posts];}classPostErrorextendsPostState{
final String message;constPostError(this.message);
@override
List<Object?>getprops=>[message];}
Events
Define the events that trigger state changes. For a simple fetch, we need an event to load posts.
The repository is responsible for fetching data from the API. It abstracts the data source and can be easily mocked in tests. Learn more about the <a href='/tech/flutter/http-client'>Flutter HTTP client</a> and <a href='/tech/flutter/dio'>Dio setup</a>.
DARTRead-only
1
import'dart:convert';import'package:http/http.dart'as http;import'../models/post.dart';
abstract classPostRepository{
Future<List<Post>>getPosts();}classPostRepositoryImplimplementsPostRepository{
final http.Client client;PostRepositoryImpl({required this.client});
@override
Future<List<Post>>getPosts() async {
final response =await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),);if(response.statusCode ==200){
final List<dynamic> jsonList = json.decode(response.body);return jsonList.map((json)=> Post.fromJson(json)).toList();}else{throwException('Failed to load posts');}}}
Bloc Implementation
Now combine the repository with events and states to create the bloc.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';import'../../models/post.dart';import'../../repositories/post_repository.dart';import'post_event.dart';import'post_state.dart';classPostBlocextendsBloc<PostEvent, PostState>{
final PostRepository repository;PostBloc(this.repository):super(PostInitial()){
on<FetchPosts>(_onFetchPosts);}
Future<void>_onFetchPosts(FetchPosts event, Emitter<PostState> emit) async {emit(PostLoading());try{
final posts =await repository.getPosts();emit(PostLoaded(posts));}catch(e){emit(PostError(e.toString()));}}}
Dependency Injection (Optional)
Use get_it to register dependencies and make them available throughout the app. This helps with testing and keeps the code clean.
DARTRead-only
1
import'package:get_it/get_it.dart';import'package:http/http.dart'as http;import'../features/posts/repositories/post_repository.dart';import'../features/posts/bloc/post_bloc.dart';
final getIt = GetIt.instance;voidinit(){
getIt.registerLazySingleton<http.Client>(()=> http.Client());
getIt.registerLazySingleton<PostRepository>(()=>PostRepositoryImpl(client:getIt()),);
getIt.registerFactory(()=>PostBloc(getIt<PostRepository>()));}
UI Integration
Provide the bloc to the widget tree using BlocProvider and listen to states with BlocBuilder/BlocConsumer.
DARTRead-only
1
import'package:flutter/material.dart';import'package:flutter_bloc/flutter_bloc.dart';import'../bloc/post_bloc.dart';import'../bloc/post_event.dart';import'../bloc/post_state.dart';classPostPageextendsStatelessWidget{
@override
Widget build(BuildContext context){returnBlocProvider(create:(_)=> getIt<PostBloc>()..add(FetchPosts()),child:Scaffold(appBar:AppBar(title:Text('Posts')),body: BlocConsumer<PostBloc, PostState>(listener:(context, state){if(state is PostError){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(state.message)),);}},builder:(context, state){if(state is PostLoading){returnCenter(child:CircularProgressIndicator());}if(state is PostLoaded){return ListView.builder(itemCount: state.posts.length,itemBuilder:(_, index){
final post = state.posts[index];returnListTile(title:Text(post.title),subtitle:Text(post.body),);},);}returnCenter(child:Text('Press button to load'));},),),);}}
Using Dio Instead of http
Dio is a more powerful HTTP client with interceptors, cancel tokens, and better error handling. Here's how to adapt the repository to use Dio.
DARTRead-only
1
import'package:dio/dio.dart';import'../models/post.dart';classPostRepositoryImplimplementsPostRepository{
final Dio dio;PostRepositoryImpl(this.dio);
@override
Future<List<Post>>getPosts() async {try{
final response =await dio.get('/posts');
final List<dynamic> jsonList = response.data;return jsonList.map((json)=> Post.fromJson(json)).toList();} on DioException catch(e){throwException('Network error: ${e.message}');}}}// In injection.dart
getIt.registerLazySingleton(()=>Dio(BaseOptions(baseUrl:'https://jsonplaceholder.typicode.com')));
http vs dio – Which One to Choose?
Verdict: Use http for quick prototypes or when you only need basic GET/POST. Switch to dio for production apps that need interceptors, cancel tokens, or advanced error handling.
Pagination Example
For infinite scrolling, extend the state to include pagination data and add a LoadMore event.
DARTRead-only
1
// StateclassPostLoadedextendsPostState{
final List<Post> posts;
final bool hasReachedMax;constPostLoaded(this.posts,{this.hasReachedMax =false});
PostLoaded copyWith({List<Post>? posts, bool? hasReachedMax}){returnPostLoaded(
posts ??this.posts,hasReachedMax: hasReachedMax ??this.hasReachedMax,);}}// EventclassLoadMorePostsextendsPostEvent{}// In Bloc
on<LoadMorePosts>(_onLoadMore);
Future<void>_onLoadMore(LoadMorePosts event, Emitter<PostState> emit) async {
final currentState = state;if(currentState is PostLoaded &&!currentState.hasReachedMax){
final more =await repository.getPosts(page: currentState.posts.length ~/10+1);if(more.isEmpty){emit(currentState.copyWith(hasReachedMax:true));}else{emit(PostLoaded(
List.of(currentState.posts)..addAll(more),));}}}
Real-World Use Cases
Social media feeds – Endless scrolling with pagination and pull‑to‑refresh.
E‑commerce product listings – Filtering, sorting, and caching.
Chat applications – Real‑time message updates with WebSockets + BLoC.
News aggregators – Offline support and background sync.
When NOT to Use BLoC
❌ Very small apps (e.g., 2–3 screens with minimal state) – Use setState or a simple ValueNotifier.
❌ One‑time API calls with no user interaction – A FutureBuilder is enough.
❌ Apps with no complex state transitions – Don't over‑engineer.
✅ When you need testability, predictability, and a clear separation of concerns – That's where BLoC shines.
Performance Tips
⚡ Cache API responses – Use a local database (Hive, SQLite) to avoid redundant network calls.
⚡ Avoid unnecessary rebuilds – Always extend Equatable in states and events.
⚡ Use BlocBuilder with buildWhen – Rebuild only when specific properties change.
⚡ Implement pagination wisely – Load more only when the user scrolls near the bottom.
⚡ Cancel pending requests – When using Dio, cancel requests if the screen is disposed to avoid memory leaks.
Best Practices
Use repositories – Keep network logic out of blocs.
Define explicit states – Loading, success, error, and optionally empty states.
Handle errors gracefully – Show user‑friendly messages and provide retry options.
Use Equatable – Prevents unnecessary rebuilds.
Test your blocs – Use bloc_test to verify state changes.
Use a service locator like get_it – Centralises dependency creation.
Cancel pending requests – When using Dio, cancel requests if the screen is disposed to avoid memory leaks.
Common Mistakes
❌ Calling API directly inside the bloc – Hard to test and change.
✅ Use a repository.
❌ Not handling network errors – App may crash.
✅ Catch exceptions and emit error states.
❌ Not using Equatable – Causes unnecessary UI rebuilds.
✅ Extend Equatable in states and events.
❌ Blocking the UI with synchronous code – Network calls should be async.
✅ Always use Future and async.
Integrating APIs with BLoC gives you a clean, reactive architecture. By separating network logic into repositories and defining explicit states, you build apps that are robust, testable, and user‑friendly. Whether you use http or dio, the pattern remains the same: events trigger state changes, and the UI reacts accordingly. For more advanced topics, explore our <a href='/tech/flutter/bloc-clean-architecture'>BLoC Clean Architecture</a> and <a href='/tech/flutter/bloc-error-handling'>error handling</a> guides.
Try it yourself
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
// Models
class Post extends Equatable {
final int id;
final int userId;
final String title;
final String body;
const Post({required this.id, required this.userId, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json['id'],
userId: json['userId'],
title: json['title'],
body: json['body'],
);
@override List<Object?> get props => [id, userId, title, body];
}
// Repository
class PostRepository {
final http.Client client;
PostRepository(this.client);
Future<List<Post>> getPosts() async {
final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
}
// States
abstract class PostState extends Equatable {
const PostState();
@override List<Object?> get props => [];
}
class PostInitial extends PostState {}
class PostLoading extends PostState {}
class PostLoaded extends PostState {
final List<Post> posts;
const PostLoaded(this.posts);
@override List<Object?> get props => [posts];
}
class PostError extends PostState {
final String message;
const PostError(this.message);
@override List<Object?> get props => [message];
}
// Events
abstract class PostEvent extends Equatable {
const PostEvent();
@override List<Object?> get props => [];
}
class FetchPosts extends PostEvent {}
// Bloc
class PostBloc extends Bloc<PostEvent, PostState> {
final PostRepository repository;
PostBloc(this.repository) : super(PostInitial()) {
on<FetchPosts>(_onFetchPosts);
}
Future<void> _onFetchPosts(FetchPosts event, Emitter<PostState> emit) async {
emit(PostLoading());
try {
final posts = await repository.getPosts();
emit(PostLoaded(posts));
} catch (e) {
emit(PostError(e.toString()));
}
}
}
// UI
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => PostBloc(PostRepository(http.Client()))..add(FetchPosts()),
child: Scaffold(
appBar: AppBar(title: Text('Posts')),
body: BlocBuilder<PostBloc, PostState>(
builder: (context, state) {
if (state is PostLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is PostLoaded) {
return ListView.builder(
itemCount: state.posts.length,
itemBuilder: (_, i) => ListTile(
title: Text(state.posts[i].title),
subtitle: Text(state.posts[i].body),
),
);
}
if (state is PostError) {
return Center(child: Text('Error: ${state.message}'));
}
return Center(child: Text('Press button to load'));
},
),
),
),
);
}
}
Test Your Knowledge
Q1
of 3
What is the purpose of a repository in BLoC API integration?
A
To store UI state
B
To abstract data fetching logic and make it testable
C
To handle navigation
D
To manage dependencies
Q2
of 3
Which state should you emit when an API request fails?
A
PostLoading
B
PostError
C
PostInitial
D
PostLoaded with empty list
Q3
of 3
Why is `Equatable` recommended for states and events?
A
It makes them serializable
B
It prevents unnecessary UI rebuilds by enabling value-based equality
C
It allows copyWith methods
D
It automatically emits states
Frequently Asked Questions
Should I use http or dio with BLoC?
Both work. dio offers more features like interceptors, cancel tokens, and form data, which are useful for larger apps. http is simpler and fine for basic needs. See the comparison table above for details.
How do I handle API token authentication?
Store the token (e.g., in get_it as a singleton) and add it to the headers in the repository. You can use dio interceptors to add it automatically.
How do I test the API integration?
Mock the repository in bloc tests. Use mockito or mocktail to return fake responses. You can also test the repository with http.Client using MockClient from the http/testing package.
How to handle slow network or timeout?
Set timeouts on the HTTP client (e.g., dio.options.connectTimeout). In the bloc, catch timeout exceptions and emit an appropriate error state.
Can I use BLoC with GraphQL?
Yes, the same pattern applies. The repository would use a GraphQL client instead of REST, but the bloc remains unchanged.