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.
API Communication Libraries
Section titled “API Communication Libraries”Flutter offers several libraries for API communication, each with distinct advantages. The two most popular options are:
HTTP vs Dio Comparison
Section titled “HTTP vs Dio Comparison”HTTP Package
Section titled “HTTP Package”Flutter’s basic HTTP client package.
Advantages:
- Simple API with minimal boilerplate
- Part of Flutter’s core packages
- Lightweight with few dependencies
Limitations:
- Basic functionality only
- Manual configuration for advanced features
- Limited built-in error handling
// pubspec.yamldependencies:http: ^1.1.0
Dio Package
Section titled “Dio Package”Feature-rich HTTP client for Flutter.
Advantages:
- Interceptors for request/response handling
- Built-in request cancellation
- Form data and file upload support
- Automatic request retries
- Download tracking
- Better error handling
Limitations:
- Slightly larger package size
- May be overkill for simple API needs
// pubspec.yamldependencies:dio: ^5.3.2
API Client Implementation
Section titled “API Client Implementation”Implementing a structured API client is essential for maintainable code. We recommend using Dio with a repository pattern:
Recommended Project Structure
Section titled “Recommended Project Structure”Directorylib
Directorycore
Directoryapi
- api_client.dart # Core API client with Dio configuration
- api_endpoints.dart # Constant endpoint definitions
- api_interceptors.dart # Custom interceptors
Directoryfeatures
Directoryfeature_1
Directorydata
Directoryrepositories
- feature_1_repository.dart # Feature-specific API calls
Base API Client
Section titled “Base API Client”// lib/core/api/api_client.dartimport 'package:dio/dio.dart';import '../../core/api/api_interceptors.dart';import '../config/app_config.dart';
class ApiClient {late Dio _dio;
// Singleton instancestatic final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
ApiClient._internal() { _dio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, connectTimeout: Duration(seconds: AppConfig.apiTimeoutSeconds), receiveTimeout: Duration(seconds: AppConfig.apiTimeoutSeconds), sendTimeout: Duration(seconds: AppConfig.apiTimeoutSeconds), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), );
// Add interceptors _dio.interceptors.add(LoggingInterceptor()); _dio.interceptors.add(AuthInterceptor()); _dio.interceptors.add(ErrorInterceptor());}
// Get Dio instanceDio get dio => _dio;
// Generic GET methodFuture<T> get<T>( String path, { Map<String, dynamic>? queryParameters, Options? options, CancelToken? cancelToken, T Function(dynamic)? parser,}) async { try { final response = await _dio.get( path, queryParameters: queryParameters, options: options, cancelToken: cancelToken, );
if (parser != null) { return parser(response.data); } else { return response.data; } } catch (e) { throw _handleError(e); }}
// Generic POST methodFuture<T> post<T>( String path, { dynamic data, Map<String, dynamic>? queryParameters, Options? options, CancelToken? cancelToken, T Function(dynamic)? parser,}) async { try { final response = await _dio.post( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, );
if (parser != null) { return parser(response.data); } else { return response.data; } } catch (e) { throw _handleError(e); }}
// Error handlingException _handleError(dynamic error) { if (error is DioException) { return ApiFailure.fromDioException(error); } return ApiFailure.unexpected(error.toString());}}
API Endpoints
Section titled “API Endpoints”// lib/core/api/api_endpoints.dartclass ApiEndpoints {// Auth endpointsstatic const String login = '/auth/login';static const String register = '/auth/register';static const String refreshToken = '/auth/refresh';
// User endpointsstatic const String userProfile = '/users/profile';static const String updateUser = '/users/update';
// Content endpointsstatic const String getProducts = '/products';static const String getProductDetails = '/products/{id}';
// Helper method for path parameter replacementstatic String replacePathParameters(String endpoint, Map<String, dynamic> parameters) { String result = endpoint; parameters.forEach((key, value) { result = result.replaceAll('{$key}', value.toString()); }); return result;}}
Feature Repository
Section titled “Feature Repository”// lib/features/products/data/repositories/product_repository.dartimport 'package:dio/dio.dart';import '../../../../core/api/api_service.dart';import '../../../../core/api/api_client.dart';import '../models/product_model.dart';
class ProductRepository {late final ApiService _apiService;
ProductRepository() { final apiClient = ApiClient(); _apiService = ApiService(apiClient.dio);}
// Clean, readable API calls with automatic parameter injectionFuture<List<Product>> getProducts({ int page = 1, int pageSize = 20, String? category,}) async { try { return await _apiService.getProducts(page, pageSize, category); } catch (e) { throw ApiFailure.fromException(e); }}
Future<Product> getProductDetails(String productId) async { try { return await _apiService.getProductDetails(productId); } catch (e) { throw ApiFailure.fromException(e); }}
Future<Product> createProduct(Product product) async { try { return await _apiService.createProduct(product); } catch (e) { throw ApiFailure.fromException(e); }}
Future<Product> updateProduct(String productId, Product product) async { try { return await _apiService.updateProduct(productId, product); } catch (e) { throw ApiFailure.fromException(e); }}
Future<void> deleteProduct(String productId) async { try { await _apiService.deleteProduct(productId); } catch (e) { throw ApiFailure.fromException(e); }}}
API Interceptors
Section titled “API Interceptors”Dio’s interceptor system enables powerful request/response processing and global error handling:
Essential Interceptors
Section titled “Essential Interceptors”// lib/core/api/api_interceptors.dartimport 'package:dio/dio.dart';import '../auth/secure_storage_service.dart';import 'dart:developer' as developer;import 'api_failure.dart';
// Authentication Interceptorclass AuthInterceptor extends Interceptor {final SecureStorageService _secureStorage = SecureStorageService();
@overrideFuture<void> onRequest( RequestOptions options, RequestInterceptorHandler handler,) async { // Get access token from secure storage final token = await _secureStorage.getAccessToken();
if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; }
return handler.next(options);}
@overrideFuture<void> onError( DioException err, ErrorInterceptorHandler handler,) async { // Handle 401 Unauthorized errors (token expired) if (err.response?.statusCode == 401) { // Get refresh token final refreshToken = await _secureStorage.getRefreshToken();
if (refreshToken != null && refreshToken.isNotEmpty) { try { // Create a new Dio instance to avoid interceptor loops final refreshDio = Dio(); final refreshResponse = await refreshDio.post( '${AppConfig.apiBaseUrl}/auth/refresh', data: {'refresh_token': refreshToken}, );
// Extract new tokens final newAccessToken = refreshResponse.data['access_token']; final newRefreshToken = refreshResponse.data['refresh_token'];
// Save new tokens await _secureStorage.saveAccessToken(newAccessToken); await _secureStorage.saveRefreshToken(newRefreshToken);
// Retry the original request with new token final requestOptions = err.requestOptions; requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
final response = await refreshDio.fetch(requestOptions); return handler.resolve(response); } catch (e) { // Refresh token is also invalid, logout user await _secureStorage.deleteAllTokens(); // TODO: Notify auth state listeners about logout } } }
// For other errors, continue with the error return handler.next(err);}}
// Logging Interceptorclass LoggingInterceptor extends Interceptor {@overridevoid onRequest( RequestOptions options, RequestInterceptorHandler handler,) { developer.log( 'REQUEST[${options.method}] => PATH: ${options.path}', name: 'API', ); return handler.next(options);}
@overridevoid onResponse( Response response, ResponseInterceptorHandler handler,) { developer.log( 'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}', name: 'API', ); return handler.next(response);}
@overridevoid onError( DioException err, ErrorInterceptorHandler handler,) { developer.log( 'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}', name: 'API', error: err.message, ); return handler.next(err);}}
// Error Interceptor using Freezed union classesclass ErrorInterceptor extends Interceptor {@overridevoid onError( DioException err, ErrorInterceptorHandler handler,) { // Convert DioExceptions to ApiFailure using Freezed union classes final apiFailure = ApiFailure.fromDioException(err);
// You can log the structured error developer.log( 'API Error: ${apiFailure.runtimeType}', name: 'API', error: apiFailure.toString(), );
// Continue with the original DioException // The repository will convert it to ApiFailure return handler.next(err);}}
Error Handling & Retries
Section titled “Error Handling & Retries”Robust error handling is crucial for a good user experience. Implement these strategies:
API Error Handling Strategies
Section titled “API Error Handling Strategies”// lib/core/api/api_error.dartclass ApiException implements Exception {final String message;final int? statusCode;final String? errorCode;final dynamic data;
ApiException({ required this.message, this.statusCode, this.errorCode, this.data,});
@overrideString toString() => 'ApiException: $message';}
// In your API clientException _handleError(dynamic error) {if (error is DioException) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return ApiException( message: 'Connection timeout. Please check your internet connection.', statusCode: error.response?.statusCode, );
case DioExceptionType.badResponse: return ApiException( message: error.error?.toString() ?? 'Server error occurred', statusCode: error.response?.statusCode, data: error.response?.data, errorCode: error.response?.data is Map ? error.response?.data['code'] : null, );
case DioExceptionType.cancel: return ApiException(message: 'Request was cancelled');
case DioExceptionType.connectionError: return ApiException( message: 'No internet connection', statusCode: error.response?.statusCode, );
default: return ApiException( message: 'Unexpected error occurred', statusCode: error.response?.statusCode, ); }}return ApiException(message: error.toString());}
Retry Mechanism
Section titled “Retry Mechanism”// lib/core/api/retry_interceptor.dartimport 'package:dio/dio.dart';import 'dart:math' as math;import 'dart:async';
class RetryInterceptor extends Interceptor {final Dio dio;final int retries;final Duration retryDelay;final List<int> retryStatusCodes;final bool useExponentialBackoff;
RetryInterceptor({ required this.dio, this.retries = 3, this.retryDelay = const Duration(seconds: 1), this.retryStatusCodes = const [408, 500, 502, 503, 504], this.useExponentialBackoff = true,});
@overrideFuture<void> onError( DioException err, ErrorInterceptorHandler handler,) async { final extraData = err.requestOptions.extra;
// Get current retry count (default 0) final retryCount = extraData['retryCount'] ?? 0;
// Check if should retry based on error type and retry count final shouldRetry = _shouldRetry(err, retryCount);
if (shouldRetry) { // Calculate delay with optional exponential backoff final delay = useExponentialBackoff ? Duration(milliseconds: retryDelay.inMilliseconds * math.pow(2, retryCount).toInt()) : retryDelay;
await Future.delayed(delay);
try { // Create a new request with the same options final options = Options( method: err.requestOptions.method, headers: err.requestOptions.headers, );
// Update retry count options.extra = { ...err.requestOptions.extra, 'retryCount': retryCount + 1, };
// Execute the request again final response = await dio.request<dynamic>( err.requestOptions.path, cancelToken: err.requestOptions.cancelToken, data: err.requestOptions.data, queryParameters: err.requestOptions.queryParameters, options: options, );
// If successful, resolve with the new response return handler.resolve(response); } catch (e) { // If retry failed, continue with the error return handler.next(err); } }
// If we shouldn't retry, continue with the error return handler.next(err);}
bool _shouldRetry(DioException err, int retryCount) { // Don't retry if we've hit the maximum retry count if (retryCount >= retries) { return false; }
// Retry on timeout errors if (err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.receiveTimeout || err.type == DioExceptionType.sendTimeout) { return true; }
// Retry on specific status codes if (err.response != null && retryStatusCodes.contains(err.response!.statusCode)) { return true; }
return false;}}
Usage in API Client
Section titled “Usage in API Client”// In your API client constructorApiClient._internal() {_dio = Dio( BaseOptions( baseUrl: 'https://api.example.com/v1', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ),);
// Add interceptors_dio.interceptors.add(LoggingInterceptor());_dio.interceptors.add(AuthInterceptor());
// Add retry interceptor_dio.interceptors.add(RetryInterceptor( dio: _dio, retries: 3, retryStatusCodes: [408, 500, 502, 503, 504], useExponentialBackoff: true,));
_dio.interceptors.add(ErrorInterceptor());}
Handling API Errors in UI
Section titled “Handling API Errors in UI”// In your UI layerFutureBuilder<List<Product>>(future: _productRepository.getProducts(),builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); }
if (snapshot.hasError) { final error = snapshot.error;
// Handle ApiFailure with pattern matching if (error is ApiFailure) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _getErrorIcon(error), size: 48, color: Colors.red, ), const SizedBox(height: 16), Text( error.userFriendlyMessage, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 16), ElevatedButton( onPressed: () => setState(() {}), child: const Text('Try Again'), ), // Show retry button only for certain errors if (_shouldShowRetryButton(error)) TextButton( onPressed: _showErrorDetails, child: const Text('Show Details'), ), ], ), ); }
// Fallback for other errors return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Something went wrong'), ElevatedButton( onPressed: () => setState(() {}), child: const Text('Try Again'), ), ], ), ); }
final products = snapshot.data!; return ListView.builder( itemCount: products.length, itemBuilder: (context, index) => ProductItem(products[index]), );},);
// Helper methods for UI error handlingIconData _getErrorIcon(ApiFailure failure) {return failure.when( serverError: (_, __, ___) => Icons.error, connectionTimeout: (_) => Icons.wifi_off, sendTimeout: (_) => Icons.upload, receiveTimeout: (_) => Icons.download, badRequest: (_, __) => Icons.warning, unauthorized: (_) => Icons.lock, forbidden: (_) => Icons.block, notFound: (_) => Icons.search_off, conflict: (_) => Icons.warning, internalServerError: (_) => Icons.error, noInternetConnection: (_) => Icons.wifi_off, requestCancelled: (_) => Icons.cancel, unexpected: (_) => Icons.error_outline,);}
bool _shouldShowRetryButton(ApiFailure failure) {return failure.when( serverError: (_, __, ___) => true, connectionTimeout: (_) => true, sendTimeout: (_) => true, receiveTimeout: (_) => true, badRequest: (_, __) => false, unauthorized: (_) => false, forbidden: (_) => false, notFound: (_) => true, conflict: (_) => false, internalServerError: (_) => true, noInternetConnection: (_) => true, requestCancelled: (_) => true, unexpected: (_) => true,);}
Secure Storage for API Keys & Tokens
Section titled “Secure Storage for API Keys & Tokens”Secure Storage Implementation
Section titled “Secure Storage Implementation”// pubspec.yamldependencies:flutter_secure_storage: ^8.0.0
// lib/core/auth/secure_storage_service.dartimport 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {final FlutterSecureStorage _storage = const FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, ),);
// Key constantsstatic const String _accessTokenKey = 'access_token';static const String _refreshTokenKey = 'refresh_token';static const String _userIdKey = 'user_id';static const String _apiKeyKey = 'api_key';
// Token managementFuture<void> saveAccessToken(String token) async { await _storage.write(key: _accessTokenKey, value: token);}
Future<String?> getAccessToken() async { return await _storage.read(key: _accessTokenKey);}
Future<void> saveRefreshToken(String token) async { await _storage.write(key: _refreshTokenKey, value: token);}
Future<String?> getRefreshToken() async { return await _storage.read(key: _refreshTokenKey);}
Future<void> saveUserId(String userId) async { await _storage.write(key: _userIdKey, value: userId);}
Future<String?> getUserId() async { return await _storage.read(key: _userIdKey);}
// API key managementFuture<void> saveApiKey(String apiKey) async { await _storage.write(key: _apiKeyKey, value: apiKey);}
Future<String?> getApiKey() async { return await _storage.read(key: _apiKeyKey);}
// Delete all tokens (logout)Future<void> deleteAllTokens() async { await _storage.delete(key: _accessTokenKey); await _storage.delete(key: _refreshTokenKey); await _storage.delete(key: _userIdKey); // Don't delete API key as it's usually app-specific, not user-specific}
// Delete everythingFuture<void> deleteAll() async { await _storage.deleteAll();}}
Using Secure Storage with API Client
Section titled “Using Secure Storage with API Client”// In your API clientclass ApiClient {late Dio _dio;final SecureStorageService _secureStorage = SecureStorageService();
// Initialize API client with API key from secure storageFuture<void> initializeWithApiKey() async { final apiKey = await _secureStorage.getApiKey(); if (apiKey != null) { _dio.options.headers['X-API-Key'] = apiKey; }}
// Non-sensitive configuration can be set directly in the constructorApiClient._internal() { _dio = Dio(BaseOptions(/* ... */)); // Add interceptors}}
App Signature Validation
Section titled “App Signature Validation”Implementing app signature validation enhances API security by ensuring requests come from your legitimate app:
Implementation
Section titled “Implementation”// pubspec.yamldependencies:package_info_plus: ^4.0.2crypto: ^3.0.3device_info_plus: ^9.0.2
// lib/core/security/app_signature.dartimport 'dart:convert';import 'package:crypto/crypto.dart';import 'package:device_info_plus/device_info_plus.dart';import 'package:package_info_plus/package_info_plus.dart';
class AppSignatureService {// Get package signature for API requestsstatic Future<Map<String, String>> getApiSignatureHeaders() async { final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); final nonce = _generateNonce(); final signature = await _generateSignature(timestamp, nonce);
return { 'X-App-Timestamp': timestamp, 'X-App-Nonce': nonce, 'X-App-Signature': signature, };}
// Generate a random nonce (number used once)static String _generateNonce() { final random = DateTime.now().millisecondsSinceEpoch.toString() + DateTime.now().microsecond.toString(); return base64.encode(utf8.encode(random)).substring(0, 16);}
// Generate HMAC signaturestatic Future<String> _generateSignature(String timestamp, String nonce) async { // Get app-specific information final packageInfo = await PackageInfo.fromPlatform(); final deviceInfoPlugin = DeviceInfoPlugin();
String deviceId = '';
// Get device-specific information if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; deviceId = androidInfo.id; } else if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; deviceId = iosInfo.identifierForVendor ?? ''; }
// Combine data for signature final dataToSign = '$timestamp:$nonce:${packageInfo.packageName}:$deviceId';
// Use your API secret key to sign // WARNING: Ideally this API_SECRET should not be stored in the app // Consider using server-side validation or other secure methods const API_SECRET = 'YOUR_API_SECRET'; // Bad practice to hardcode this
// Generate HMAC signature final hmacSha256 = Hmac(sha256, utf8.encode(API_SECRET)); final digest = hmacSha256.convert(utf8.encode(dataToSign));
return digest.toString();}}
Data Serialization
Section titled “Data Serialization”Proper data serialization ensures efficient conversion between API JSON data and Dart objects:
Data Serialization Approaches
Section titled “Data Serialization Approaches”// lib/features/products/data/models/product_model.dartclass Product {final String id;final String name;final double price;final String? description;final List<String> imageUrls;final DateTime createdAt;
Product({ required this.id, required this.name, required this.price, this.description, required this.imageUrls, required this.createdAt,});
// Manual fromJson constructorfactory Product.fromJson(Map<String, dynamic> json) { return Product( id: json['id'], name: json['name'], price: (json['price'] as num).toDouble(), description: json['description'], imageUrls: (json['image_urls'] as List<dynamic>) .map((url) => url as String) .toList(), createdAt: DateTime.parse(json['created_at']), );}
// Manual toJson methodMap<String, dynamic> toJson() { return { 'id': id, 'name': name, 'price': price, 'description': description, 'image_urls': imageUrls, 'created_at': createdAt.toIso8601String(), };}}
When to Use:
- For simple models with few fields
- When you want full control over serialization logic
- For learning purposes to understand how serialization works
// pubspec.yamldependencies:json_annotation: ^4.8.1
dev_dependencies:build_runner: ^2.3.3json_serializable: ^6.7.1
// lib/features/products/data/models/product_model.dartimport 'package:json_annotation/json_annotation.dart';
part 'product_model.g.dart';
@JsonSerializable()class Product {final String id;final String name;final double price;final String? description;
@JsonKey(name: 'image_urls')final List<String> imageUrls;
@JsonKey(name: 'created_at', fromJson: _dateTimeFromString)final DateTime createdAt;
Product({ required this.id, required this.name, required this.price, this.description, required this.imageUrls, required this.createdAt,});
// Generated fromJson constructorfactory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
// Generated toJson methodMap<String, dynamic> toJson() => _$ProductToJson(this);
// Custom date parserstatic DateTime _dateTimeFromString(String dateString) { return DateTime.parse(dateString);}}
// Run this command to generate serialization codeflutter pub run build_runner build --delete-conflicting-outputs
When to Use:
- For medium to complex models
- When you need to maintain clean model classes
- For projects with many model classes
// pubspec.yamldependencies:freezed_annotation: ^2.4.1json_annotation: ^4.8.1
dev_dependencies:build_runner: ^2.3.3freezed: ^2.4.2json_serializable: ^6.7.1
// lib/features/products/data/models/product_model.dartimport 'package:freezed_annotation/freezed_annotation.dart';
part 'product_model.freezed.dart';part 'product_model.g.dart';
@freezedclass Product with _$Product {const factory Product({ required String id, required String name, required double price, String? description,
@JsonKey(name: 'image_urls') required List<String> imageUrls,
@JsonKey(name: 'created_at') required DateTime createdAt,}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);}
// Run this command to generate serialization codeflutter pub run build_runner build --delete-conflicting-outputs
When to Use:
- For complex models
- When you need immutable data classes
- When you need pattern matching
- For comprehensive model generation with minimal boilerplate
Handling Nested Objects and Lists
Section titled “Handling Nested Objects and Lists”// lib/features/products/data/models/product_model.dart@JsonSerializable(explicitToJson: true)class Product {final String id;final String name;final double price;final List<ProductVariant> variants;final ProductCategory category;
Product({ required this.id, required this.name, required this.price, required this.variants, required this.category,});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);Map<String, dynamic> toJson() => _$ProductToJson(this);}
@JsonSerializable()class ProductVariant {final String id;final String name;final double price;
ProductVariant({ required this.id, required this.name, required this.price,});
factory ProductVariant.fromJson(Map<String, dynamic> json) => _$ProductVariantFromJson(json);Map<String, dynamic> toJson() => _$ProductVariantToJson(this);}
@JsonSerializable()class ProductCategory {final String id;final String name;
ProductCategory({ required this.id, required this.name,});
factory ProductCategory.fromJson(Map<String, dynamic> json) => _$ProductCategoryFromJson(json);Map<String, dynamic> toJson() => _$ProductCategoryToJson(this);}
JSON Serialization Best Practices
Section titled “JSON Serialization Best Practices”-
Use Consistent Naming Conventions Clearly map API snake_case to Dart camelCase using @JsonKey.
-
Handle Null Values Properly Make non-nullable fields required in constructors and provide defaults for nullable fields.
-
Type Safety First Always convert dynamic values to proper types with validation.
-
Custom Converters for Complex Types Use fromJson/toJson helpers for dates, enums, and custom types.
-
Prefer Code Generation Use json_serializable or freezed to minimize manual serialization errors.
Pro Tips for Data Serialization
Section titled “Pro Tips for Data Serialization”- Create base model classes with common serialization patterns
- Write unit tests for your serialization logic
- Use QuickType to quickly generate model classes from JSON samples
- Consider nullable fields carefully in your models
- Validate incoming API data to prevent runtime errors
API Integration Best Practices Summary
Section titled “API Integration Best Practices Summary”Best Practices Checklist
Section titled “Best Practices Checklist”- Use Dio for advanced API features and interceptors
- Implement retry mechanisms for transient network failures
- Store sensitive data in secure storage
- Implement certificate pinning for critical APIs
- Use code generation for data serialization
- Handle errors gracefully in the UI
- Structure API clients using repository pattern
- Validate API responses before parsing
- Implement proper token refresh logic
- Monitor API performance using logging interceptors
Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”- Hardcoding API keys in source code
- Not handling connection errors properly
- Using too many global interceptors that slow down requests
- Blocking the UI thread during API calls
- Not caching API responses for frequently used data
- Ignoring API versioning in your client code
- Skipping proper error handling in API repositories
See Also
Section titled “See Also”- Dio Package Documentation – Feature-rich HTTP client for Flutter
- Flutter Secure Storage – Secure storage for sensitive data
- json_serializable Package – JSON serialization through code generation
- freezed Package – Code generation for immutable classes