Flutter Networking Patterns
Dio Setup with Interceptors
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {'Accept': 'application/json'},
));
dio.interceptors.addAll([
AuthInterceptor(ref),
LogInterceptor(requestBody: true, responseBody: true),
RetryInterceptor(dio: dio, retries: 3),
]);
return dio;
});
Auth Interceptor
class AuthInterceptor extends Interceptor {
AuthInterceptor(this.ref);
final Ref ref;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = ref.read(authTokenProvider);
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException e, ErrorInterceptorHandler handler) async {
if (e.response?.statusCode == 401) {
try {
final newToken = await ref.read(authServiceProvider).refreshToken();
ref.read(authTokenProvider.notifier).update(newToken);
// Retry request with new token
e.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await ref.read(dioProvider).fetch(e.requestOptions);
return handler.resolve(response);
} catch (_) {
ref.read(authServiceProvider).logout();
}
}
handler.next(e);
}
}
Repository Pattern
abstract class UserRepository {
Future<User> fetchUser(String id);
Future<List<User>> fetchUsers({int page = 1, int limit = 20});
Future<User> createUser(CreateUserDto dto);
}
class DioUserRepository implements UserRepository {
DioUserRepository(this._dio);
final Dio _dio;
@override
Future<User> fetchUser(String id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
}
@override
Future<List<User>> fetchUsers({int page = 1, int limit = 20}) async {
final response = await _dio.get('/users', queryParameters: {
'page': page,
'limit': limit,
});
return (response.data['data'] as List)
.map((j) => User.fromJson(j as Map<String, dynamic>))
.toList();
}
@override
Future<User> createUser(CreateUserDto dto) async {
final response = await _dio.post('/users', data: dto.toJson());
return User.fromJson(response.data as Map<String, dynamic>);
}
}
Error Handling
sealed class ApiError {
const ApiError();
}
class NetworkError extends ApiError { const NetworkError(); }
class ServerError extends ApiError { final int code; const ServerError(this.code); }
class AuthError extends ApiError { const AuthError(); }
class UnknownError extends ApiError { final Object error; const UnknownError(this.error); }
ApiError mapDioError(DioException e) => switch (e.type) {
DioExceptionType.connectionTimeout ||
DioExceptionType.receiveTimeout => const NetworkError(),
DioExceptionType.badResponse => switch (e.response?.statusCode) {
401 => const AuthError(),
>= 500 => ServerError(e.response!.statusCode!),
_ => UnknownError(e),
},
_ => UnknownError(e),
};
// Usage in repository
Future<Result<User>> fetchUserSafe(String id) async {
try {
final user = await fetchUser(id);
return Success(user);
} on DioException catch (e) {
return Failure(mapDioError(e).toString());
}
}
JSON Serialization
// Manual (no codegen)
class User {
const User({required this.id, required this.name, this.avatar});
final String id;
final String name;
final String? avatar;
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
name: json['name'] as String,
avatar: json['avatar'] as String?,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
if (avatar != null) 'avatar': avatar,
};
}
// With json_serializable (pubspec: json_serializable, json_annotation, build_runner)
@JsonSerializable()
class User {
const User({required this.id, required this.name});
final String id;
@JsonKey(name: 'display_name')
final String name;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Caching with dio_cache_interceptor
final cacheStore = MemCacheStore(maxSize: 10485760, maxEntrySize: 1048576);
dio.interceptors.add(DioCacheInterceptor(
options: CacheOptions(
store: cacheStore,
policy: CachePolicy.refreshForceCache,
maxStale: const Duration(minutes: 5),
),
));
Multipart / File Upload
Future<void> uploadAvatar(String filePath) async {
final formData = FormData.fromMap({
'avatar': await MultipartFile.fromFile(filePath, filename: 'avatar.jpg'),
'userId': currentUserId,
});
await _dio.post('/users/avatar', data: formData,
onSendProgress: (sent, total) => print('${(sent / total * 100).toInt()}%'));
}
Cancellation
final cancelToken = CancelToken();
Future<void> fetchWithCancel() async {
try {
await _dio.get('/slow-endpoint', cancelToken: cancelToken);
} on DioException catch (e) {
if (CancelToken.isCancel(e)) print('Request cancelled');
}
}
// Cancel from outside (e.g., dispose)
cancelToken.cancel('Widget disposed');