# Networking Dio configuration, interceptors, error handling, and caching strategies for Flutter network requests. ## Dio Setup ```dart import 'package:dio/dio.dart'; class ApiClient { static final ApiClient _instance = ApiClient._internal(); factory ApiClient() => _instance; late final Dio dio; ApiClient._internal() { dio = Dio(BaseOptions( baseUrl: 'https://api.example.com/v1', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), sendTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, )); dio.interceptors.addAll([ AuthInterceptor(), LoggingInterceptor(), RetryInterceptor(dio: dio), ]); } } ``` ## Interceptors ### Auth Interceptor ```dart class AuthInterceptor extends Interceptor { final TokenStorage _tokenStorage; AuthInterceptor({TokenStorage? tokenStorage}) : _tokenStorage = tokenStorage ?? TokenStorage(); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { final token = await _tokenStorage.getAccessToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401) { try { final newToken = await _refreshToken(); if (newToken != null) { err.requestOptions.headers['Authorization'] = 'Bearer $newToken'; final response = await Dio().fetch(err.requestOptions); return handler.resolve(response); } } catch (e) { await _tokenStorage.clearTokens(); } } handler.next(err); } Future _refreshToken() async { final refreshToken = await _tokenStorage.getRefreshToken(); if (refreshToken == null) return null; final response = await Dio().post( 'https://api.example.com/v1/auth/refresh', data: {'refresh_token': refreshToken}, ); if (response.statusCode == 200) { final newToken = response.data['access_token']; await _tokenStorage.saveAccessToken(newToken); return newToken; } return null; } } ``` ### Logging Interceptor ```dart class LoggingInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { debugPrint('→ ${options.method} ${options.uri}'); if (options.data != null) { debugPrint(' Body: ${options.data}'); } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { debugPrint('← ${response.statusCode} ${response.requestOptions.uri}'); handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) { debugPrint('✗ ${err.response?.statusCode} ${err.requestOptions.uri}'); debugPrint(' Error: ${err.message}'); handler.next(err); } } ``` ### Retry Interceptor ```dart class RetryInterceptor extends Interceptor { final Dio dio; final int maxRetries; final Duration retryDelay; RetryInterceptor({ required this.dio, this.maxRetries = 3, this.retryDelay = const Duration(seconds: 1), }); @override void onError(DioException err, ErrorInterceptorHandler handler) async { final retryCount = err.requestOptions.extra['retryCount'] ?? 0; if (_shouldRetry(err) && retryCount < maxRetries) { await Future.delayed(retryDelay * (retryCount + 1)); err.requestOptions.extra['retryCount'] = retryCount + 1; try { final response = await dio.fetch(err.requestOptions); return handler.resolve(response); } catch (e) { return handler.next(err); } } handler.next(err); } bool _shouldRetry(DioException err) { return err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.sendTimeout || err.type == DioExceptionType.receiveTimeout || (err.response?.statusCode ?? 0) >= 500; } } ``` ## Error Handling ### Custom Exception ```dart sealed class ApiException implements Exception { final String message; final int? statusCode; final dynamic data; const ApiException({ required this.message, this.statusCode, this.data, }); } class NetworkException extends ApiException { const NetworkException({super.message = 'Network connection failed'}); } class ServerException extends ApiException { const ServerException({ required super.message, super.statusCode, super.data, }); } class UnauthorizedException extends ApiException { const UnauthorizedException({super.message = 'Authentication required'}); } class ValidationException extends ApiException { final Map> errors; const ValidationException({ required this.errors, super.message = 'Validation failed', }); } ``` ### Error Handler ```dart class ApiErrorHandler { static ApiException handle(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return const NetworkException(message: 'Connection timeout'); case DioExceptionType.connectionError: return const NetworkException(message: 'No internet connection'); case DioExceptionType.badResponse: return _handleResponse(error.response); case DioExceptionType.cancel: return const ApiException(message: 'Request cancelled'); default: return ApiException(message: error.message ?? 'Unknown error'); } } static ApiException _handleResponse(Response? response) { final statusCode = response?.statusCode ?? 0; final data = response?.data; switch (statusCode) { case 400: if (data is Map && data.containsKey('errors')) { return ValidationException( errors: Map>.from( (data['errors'] as Map).map( (k, v) => MapEntry(k.toString(), List.from(v)), ), ), ); } return ServerException( message: data?['message'] ?? 'Bad request', statusCode: statusCode, ); case 401: return const UnauthorizedException(); case 403: return const ServerException( message: 'Access denied', statusCode: 403, ); case 404: return const ServerException( message: 'Resource not found', statusCode: 404, ); case 422: return ValidationException( errors: _parseValidationErrors(data), ); case >= 500: return ServerException( message: 'Server error', statusCode: statusCode, ); default: return ServerException( message: data?['message'] ?? 'Unknown error', statusCode: statusCode, ); } } static Map> _parseValidationErrors(dynamic data) { if (data is! Map) return {}; final errors = data['errors']; if (errors is! Map) return {}; return errors.map((k, v) => MapEntry( k.toString(), v is List ? v.map((e) => e.toString()).toList() : [v.toString()], )); } } ``` ## Repository Pattern ```dart abstract class BaseRepository { final Dio dio; BaseRepository(this.dio); Future safeCall(Future Function() call) async { try { return await call(); } on DioException catch (e) { throw ApiErrorHandler.handle(e); } } } class UserRepository extends BaseRepository { UserRepository(super.dio); Future getUser(String id) => safeCall(() async { final response = await dio.get('/users/$id'); return User.fromJson(response.data); }); Future> getUsers({int page = 1, int limit = 20}) => safeCall(() async { final response = await dio.get('/users', queryParameters: { 'page': page, 'limit': limit, }); return (response.data['data'] as List) .map((e) => User.fromJson(e)) .toList(); }); Future updateUser(String id, Map data) => safeCall(() async { final response = await dio.patch('/users/$id', data: data); return User.fromJson(response.data); }); } ``` ## Caching ### Memory Cache ```dart class CacheInterceptor extends Interceptor { final Map _cache = {}; final Duration maxAge; CacheInterceptor({this.maxAge = const Duration(minutes: 5)}); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (options.method != 'GET') { handler.next(options); return; } final key = _cacheKey(options); final cached = _cache[key]; if (cached != null && !cached.isExpired) { return handler.resolve(Response( requestOptions: options, data: cached.data, statusCode: 200, )); } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { if (response.requestOptions.method == 'GET') { final key = _cacheKey(response.requestOptions); _cache[key] = CacheEntry( data: response.data, expiry: DateTime.now().add(maxAge), ); } handler.next(response); } String _cacheKey(RequestOptions options) { return '${options.uri}'; } void invalidate(String pattern) { _cache.removeWhere((key, _) => key.contains(pattern)); } void clear() => _cache.clear(); } class CacheEntry { final dynamic data; final DateTime expiry; CacheEntry({required this.data, required this.expiry}); bool get isExpired => DateTime.now().isAfter(expiry); } ``` ### Disk Cache with Hive ```dart import 'package:hive_flutter/hive_flutter.dart'; class DiskCacheInterceptor extends Interceptor { static const String _boxName = 'api_cache'; final Duration maxAge; DiskCacheInterceptor({this.maxAge = const Duration(hours: 1)}); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { if (options.method != 'GET') { handler.next(options); return; } final box = await Hive.openBox(_boxName); final key = _cacheKey(options); final cached = box.get(key); if (cached != null) { final entry = CachedResponse.fromJson(cached); if (!entry.isExpired) { return handler.resolve(Response( requestOptions: options, data: entry.data, statusCode: 200, )); } } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) async { if (response.requestOptions.method == 'GET') { final box = await Hive.openBox(_boxName); final key = _cacheKey(response.requestOptions); await box.put(key, CachedResponse( data: response.data, expiry: DateTime.now().add(maxAge), ).toJson()); } handler.next(response); } String _cacheKey(RequestOptions options) => options.uri.toString(); } class CachedResponse { final dynamic data; final DateTime expiry; CachedResponse({required this.data, required this.expiry}); bool get isExpired => DateTime.now().isAfter(expiry); factory CachedResponse.fromJson(Map json) { return CachedResponse( data: json['data'], expiry: DateTime.parse(json['expiry']), ); } Map toJson() => { 'data': data, 'expiry': expiry.toIso8601String(), }; } ``` ## Riverpod Integration ```dart @riverpod Dio dio(Ref ref) { return ApiClient().dio; } @riverpod UserRepository userRepository(Ref ref) { return UserRepository(ref.watch(dioProvider)); } @riverpod Future user(Ref ref, String id) async { final repository = ref.watch(userRepositoryProvider); return repository.getUser(id); } @riverpod class Users extends _$Users { @override Future> build() => _fetch(); Future> _fetch() async { final repository = ref.watch(userRepositoryProvider); return repository.getUsers(); } Future refresh() async { state = const AsyncLoading(); state = await AsyncValue.guard(_fetch); } } ``` ## Request Cancellation ```dart class SearchRepository extends BaseRepository { CancelToken? _searchToken; SearchRepository(super.dio); Future> search(String query) async { _searchToken?.cancel(); _searchToken = CancelToken(); return safeCall(() async { final response = await dio.get( '/search', queryParameters: {'q': query}, cancelToken: _searchToken, ); return (response.data as List) .map((e) => SearchResult.fromJson(e)) .toList(); }); } } ``` ## Common Patterns | Pattern | Usage | |---------|-------| | Singleton client | Single Dio instance across app | | Interceptor chain | Auth → Retry → Cache → Logging | | Repository layer | Abstract API from business logic | | Error mapping | Convert DioException to app exceptions | | Cancel tokens | Debounce/cancel previous requests | | Cache invalidation | Clear cache on mutations | ## Networking Checklist | Item | Implementation | |------|----------------| | Base configuration | Timeouts, headers, base URL | | Auth handling | Token injection, refresh on 401 | | Error handling | Typed exceptions, user messages | | Retry logic | Exponential backoff for transient errors | | Request logging | Debug interceptor | | Caching | Memory/disk cache for GET requests | | Cancellation | Cancel tokens for search/debounce | --- *Dio is an open-source package by the Flutter China community.*