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.
Choosing the Right HTTP Client
Section titled “Choosing the Right HTTP Client”Flutter offers several libraries for API communication, each with distinct advantages for different use cases:
HTTP Package (Simple)
Section titled “HTTP Package (Simple)”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.0Basic usage example:
// Making a simple GET request with authenticationfinal response = await http.get(Uri.parse('https://api.example.com/users'),headers: { 'Authorization': 'Bearer $token',},);
// Check response statusif (response.statusCode == 200) {// Parse the response bodyfinal data = jsonDecode(response.body);print('Users: $data');}Dio Package (Advanced)
Section titled “Dio Package (Advanced)”Feature-rich HTTP client with advanced capabilities:
- Built-in interceptors for request/response handling: Automatic token management and logging
- Request cancellation and retry mechanisms: Better user experience during network issues
- Form data and file upload support: Comprehensive data handling capabilities
- Advanced error handling: Structured error responses with detailed information
Use Case: Recommended for production apps requiring robust API communication and advanced features.
Add to your pubspec.yaml:
dependencies:dio: ^5.3.2Advanced usage with interceptors:
// Create Dio instance with interceptorsfinal dio = Dio()..interceptors.add(AuthInterceptor()) // Automatically adds auth tokens..interceptors.add(LoggingInterceptor()); // Logs all requests/responses
// Making a requestfinal response = await dio.get('/users');API Client Architecture
Section titled “API Client Architecture”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.
Project Structure
Section titled “Project Structure”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
Base API Client Setup
Section titled “Base API Client Setup”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.
// lib/core/api/api_client.dartclass ApiClient {late Dio _dio;
// Singleton pattern ensures one instance across the appstatic 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 requestsDio get dio => _dio;}Essential Interceptors
Section titled “Essential Interceptors”Interceptors allow you to intercept and modify requests/responses globally. They’re perfect for adding authentication headers, logging, and error handling.
// lib/core/api/api_interceptors.dart
// Authentication Interceptor - Adds auth token to every requestclass AuthInterceptor extends Interceptor {@overrideFuture<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 ApiFailureclass ErrorInterceptor extends Interceptor {@overridevoid onError(DioException err, ErrorInterceptorHandler handler) { // Convert to structured error final apiError = ApiFailure.fromDioException(err);
// Continue with error handling handler.next(err);}}Error Handling Strategy
Section titled “Error Handling Strategy”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.
Structured Error Classes
Section titled “Structured Error Classes”Create a hierarchy of error classes to represent different API failure scenarios. This approach makes error handling more predictable and testable.
// lib/core/api/api_failure.dart
// Base class for all API failuresabstract class ApiFailure {final String message;final int? statusCode;
const ApiFailure(this.message, this.statusCode);
// Factory constructor to convert DioException to ApiFailurefactory 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 scenariosclass ConnectionTimeout extends ApiFailure {const ConnectionTimeout() : super('Connection timeout', null);}
class ServerError extends ApiFailure {const ServerError(int? statusCode) : super('Server error', statusCode);}Repository Implementation
Section titled “Repository Implementation”Repositories encapsulate API calls for specific features. They handle the conversion between API responses and domain models, and translate exceptions into structured failures.
// lib/features/user_profile/domain/repositories/user_repository.dartclass UserRepository {final ApiClient _apiClient;
UserRepository(this._apiClient);
// Fetch user by IDFuture<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 informationFuture<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); }}}Secure Storage for Authentication
Section titled “Secure Storage for Authentication”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.0Implement secure storage service:
// lib/core/storage/secure_storage.dartclass SecureStorage {// Initialize secure storage with platform-specific optionsstatic const _storage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, // Use encrypted shared preferences ),);
// Save access token securelyFuture<void> saveAccessToken(String token) async { await _storage.write( key: 'access_token', value: token, );}
// Retrieve access tokenFuture<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');}}Data Serialization Approaches
Section titled “Data Serialization Approaches”Choose the right serialization approach based on your project’s complexity. Serialization converts JSON data from APIs into Dart objects and vice versa.
Manual JSON Handling
Section titled “Manual JSON Handling”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 objectfactory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], );}
// Convert User object to JSONMap<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.
Code Generation (Recommended)
Section titled “Code Generation (Recommended)”Automated serialization with code generation. Reduces boilerplate and minimizes serialization errors.
Add dependencies:
dependencies:json_annotation: ^4.8.1
dev_dependencies:build_runner: ^2.3.3json_serializable: ^6.7.1Create model with annotations:
import 'package:json_annotation/json_annotation.dart';
// Include generated filepart 'user.g.dart';
@JsonSerializable()class User {final String id;final String name;
// Map API field name to Dart property@JsonKey(name: 'email_address')final String email;
User({ required this.id, required this.name, required this.email,});
// Generated fromJson methodfactory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// Generated toJson methodMap<String, dynamic> toJson() => _$UserToJson(this);}Generate serialization code:
# Run code generationflutter pub run build_runner build
# Watch for changes (development)flutter pub run build_runner watchWhen to Use: Medium to large projects with multiple models that need consistent serialization patterns.
Immutable Data Classes
Section titled “Immutable Data Classes”Comprehensive code generation with immutability. Perfect for complex applications requiring pattern matching and immutable data structures.
Add dependencies:
dependencies:freezed_annotation: ^2.4.1
dev_dependencies:freezed: ^2.4.2json_serializable: ^6.7.1build_runner: ^2.3.3Create immutable model:
import 'package:freezed_annotation/freezed_annotation.dart';
// Include generated filespart 'user.freezed.dart';part 'user.g.dart';
@freezedclass User with _$User {const factory User({ required String id, required String name, required String email,}) = _User;
// Generated fromJson methodfactory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);}Generate code:
# Run code generationflutter pub run build_runner build --delete-conflicting-outputsWhen to Use: Complex applications requiring immutable data classes, pattern matching, and comprehensive model generation.
Handling API Errors in UI
Section titled “Handling API Errors in UI”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.
// In your BLoC or Cubitclass 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 widgetBlocBuilder<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();},)Best Practices Summary
Section titled “Best Practices Summary”API Integration Checklist
Section titled “API Integration Checklist”-
Choose the right HTTP client based on your app’s complexity
-
Structure API clients using the repository pattern
-
Implement proper error handling with structured error classes
-
Use secure storage for sensitive authentication data
-
Choose appropriate serialization based on model complexity
-
Handle network errors gracefully in the UI layer
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”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
Performance Considerations
Section titled “Performance Considerations”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.
See Also
Section titled “See Also”- State Management – How to integrate API calls with state management
- Project Structure – Organizing API-related code in your project
- Dio Package Documentation – Comprehensive HTTP client for Flutter
- Flutter Secure Storage – Secure storage for sensitive data