Skip to content

API Integration & Data Handling

Effective API integration is essential for modern Flutter applications that rely on backend services. Implementing secure, efficient, and resilient communication patterns ensures your app delivers a seamless user experience while protecting sensitive data.

Flutter offers several libraries for API communication, each with distinct advantages for different use cases:

Flutter’s basic HTTP client package for straightforward API needs:

  • Simple API with minimal boilerplate: Quick setup for basic requests
  • Part of Flutter’s core packages: No additional dependencies
  • Lightweight implementation: Perfect for simple API interactions

Use Case: Best for small apps with basic API requirements and minimal complexity.

Add to your pubspec.yaml:

dependencies:
http: ^1.1.0

Basic usage example:

// Making a simple GET request with authentication
final response = await http.get(
Uri.parse('https://api.example.com/users'),
headers: {
'Authorization': 'Bearer $token',
},
);
// Check response status
if (response.statusCode == 200) {
// Parse the response body
final data = jsonDecode(response.body);
print('Users: $data');
}

Structure your API client using a repository pattern for better maintainability and testing. This approach separates network logic from business logic, making your code more testable and easier to maintain.

  • Directorylib
    • Directorycore
      • Directoryapi
        • api_client.dart # Core Dio configuration
        • api_endpoints.dart # Route constants
        • api_interceptors.dart # Auth, logging, error handling
    • Directoryfeatures
      • Directoryuser_profile
        • Directorydomain
          • Directoryrepositories
            • user_repository.dart # Feature-specific API calls

The API client is the foundation of your network layer. It configures Dio with base settings and adds interceptors for cross-cutting concerns like authentication and logging.

API Client Configuration
// lib/core/api/api_client.dart
class ApiClient {
late Dio _dio;
// Singleton pattern ensures one instance across the app
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
ApiClient._internal() {
// Configure Dio with base settings
_dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl, // Base URL from config
connectTimeout: const Duration(seconds: 30), // Connection timeout
headers: {
'Content-Type': 'application/json', // Default content type
},
),
);
// Add interceptors in order of execution
_dio.interceptors.addAll([
AuthInterceptor(), // Handles authentication tokens
LoggingInterceptor(), // Logs requests/responses for debugging
ErrorInterceptor(), // Converts errors to ApiFailure
]);
}
// Expose Dio instance for making requests
Dio get dio => _dio;
}

Interceptors allow you to intercept and modify requests/responses globally. They’re perfect for adding authentication headers, logging, and error handling.

Core Interceptors
// lib/core/api/api_interceptors.dart
// Authentication Interceptor - Adds auth token to every request
class AuthInterceptor extends Interceptor {
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// Retrieve token from secure storage
final token = await SecureStorage().getAccessToken();
// Add token to request headers if available
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// Continue with the request
handler.next(options);
}
}
// Error Interceptor - Converts DioException to ApiFailure
class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// Convert to structured error
final apiError = ApiFailure.fromDioException(err);
// Continue with error handling
handler.next(err);
}
}

Implement comprehensive error handling to provide users with meaningful feedback. Structured error classes make it easier to handle different error scenarios consistently across your app.

Create a hierarchy of error classes to represent different API failure scenarios. This approach makes error handling more predictable and testable.

Error Handling Classes
// lib/core/api/api_failure.dart
// Base class for all API failures
abstract class ApiFailure {
final String message;
final int? statusCode;
const ApiFailure(this.message, this.statusCode);
// Factory constructor to convert DioException to ApiFailure
factory ApiFailure.fromDioException(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
return ConnectionTimeout();
case DioExceptionType.badResponse:
return ServerError(error.response?.statusCode);
default:
return UnexpectedError(error.message);
}
}
}
// Specific error types for different scenarios
class ConnectionTimeout extends ApiFailure {
const ConnectionTimeout() : super('Connection timeout', null);
}
class ServerError extends ApiFailure {
const ServerError(int? statusCode)
: super('Server error', statusCode);
}

Repositories encapsulate API calls for specific features. They handle the conversion between API responses and domain models, and translate exceptions into structured failures.

Repository Pattern
// lib/features/user_profile/domain/repositories/user_repository.dart
class UserRepository {
final ApiClient _apiClient;
UserRepository(this._apiClient);
// Fetch user by ID
Future<User> getUser(String userId) async {
try {
// Make API request
final response = await _apiClient.dio.get('/users/$userId');
// Parse response to domain model
return User.fromJson(response.data);
} catch (e) {
// Convert any error to ApiFailure
throw ApiFailure.fromException(e);
}
}
// Update user information
Future<User> updateUser(User user) async {
try {
// Send updated user data
final response = await _apiClient.dio.put(
'/users/${user.id}',
data: user.toJson(), // Convert model to JSON
);
// Return updated user
return User.fromJson(response.data);
} catch (e) {
throw ApiFailure.fromException(e);
}
}
}

Protect sensitive data like tokens and API keys using secure storage. The flutter_secure_storage package encrypts data before storing it on the device.

Add the dependency:

dependencies:
flutter_secure_storage: ^9.0.0

Implement secure storage service:

Secure Token Storage
// lib/core/storage/secure_storage.dart
class SecureStorage {
// Initialize secure storage with platform-specific options
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true, // Use encrypted shared preferences
),
);
// Save access token securely
Future<void> saveAccessToken(String token) async {
await _storage.write(
key: 'access_token',
value: token,
);
}
// Retrieve access token
Future<String?> getAccessToken() async {
return await _storage.read(key: 'access_token');
}
// Delete all authentication tokens (logout)
Future<void> deleteAllTokens() async {
await _storage.delete(key: 'access_token');
await _storage.delete(key: 'refresh_token');
}
}

Choose the right serialization approach based on your project’s complexity. Serialization converts JSON data from APIs into Dart objects and vice versa.

Simple approach for small models with few fields. You have full control over the serialization logic.

class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
// Convert JSON to User object
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
// Convert User object to JSON
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}

When to Use: Simple models, learning purposes, or when you need full control over serialization logic.


Create user-friendly error handling in your UI components. The UI should gracefully handle different error states and provide users with clear feedback and recovery options.

UI Error Handling
// In your BLoC or Cubit
class UserCubit extends Cubit<UserState> {
final UserRepository _repository;
UserCubit(this._repository) : super(UserInitial());
Future<void> loadUser(String userId) async {
// Emit loading state
emit(UserLoading());
try {
// Fetch user from repository
final user = await _repository.getUser(userId);
// Emit success state with data
emit(UserLoaded(user));
} on ApiFailure catch (failure) {
// Emit error state with message
emit(UserError(failure.message));
}
}
}
// In your UI widget
BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
// Handle loading state
if (state is UserLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
// Handle success state
else if (state is UserLoaded) {
return UserProfile(user: state.user);
}
// Handle error state
else if (state is UserError) {
return Center(
child: ErrorWidget(
message: state.message,
// Provide retry functionality
onRetry: () => context
.read<UserCubit>()
.loadUser(userId),
),
);
}
// Handle initial state
return const SizedBox.shrink();
},
)

  1. Choose the right HTTP client based on your app’s complexity

  2. Structure API clients using the repository pattern

  3. Implement proper error handling with structured error classes

  4. Use secure storage for sensitive authentication data

  5. Choose appropriate serialization based on model complexity

  6. Handle network errors gracefully in the UI layer

Hardcoding API endpoints

  • Use constants and configuration files to manage API URLs
  • Keep environment-specific URLs separate (dev, staging, production)

Ignoring timeout configurations

  • Always set appropriate timeouts for connect, receive, and send operations
  • Consider different timeout values for different types of requests

Not handling offline scenarios

  • Implement proper connectivity checks before making requests
  • Provide meaningful feedback when the device is offline
  • Consider caching strategies for offline functionality

Exposing sensitive data in logs

  • Be careful with logging interceptors in production builds
  • Never log authentication tokens or personal user data
  • Use conditional logging based on build mode

Blocking the UI thread

  • Always use async/await for API calls
  • Never make synchronous network requests
  • Show loading indicators during API operations

Request Optimization: Implement caching strategies for frequently accessed data and consider request debouncing for search functionality.

Memory Management: Dispose of HTTP clients properly and avoid memory leaks in long-running operations.

Network Efficiency: Use compression, implement pagination for large datasets, and consider request batching where appropriate.


  1. State Management – How to integrate API calls with state management
  2. Project Structure – Organizing API-related code in your project
  3. Dio Package Documentation – Comprehensive HTTP client for Flutter
  4. Flutter Secure Storage – Secure storage for sensitive data