122 lines
3.2 KiB
Dart
122 lines
3.2 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../constants.dart';
|
|
|
|
/// Standard error format returned by the API.
|
|
class ApiError {
|
|
final String code;
|
|
final String message;
|
|
final int statusCode;
|
|
|
|
ApiError({
|
|
required this.code,
|
|
required this.message,
|
|
required this.statusCode,
|
|
});
|
|
|
|
factory ApiError.fromResponse(Response response) {
|
|
final data = response.data;
|
|
if (data is Map<String, dynamic> && data.containsKey('error')) {
|
|
final error = data['error'] as Map<String, dynamic>;
|
|
return ApiError(
|
|
code: error['code'] as String? ?? 'UNKNOWN',
|
|
message: error['message'] as String? ?? 'Unknown error',
|
|
statusCode: response.statusCode ?? 500,
|
|
);
|
|
}
|
|
return ApiError(
|
|
code: 'UNKNOWN',
|
|
message: 'Unexpected error',
|
|
statusCode: response.statusCode ?? 500,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() => 'ApiError($code: $message)';
|
|
}
|
|
|
|
class ApiException implements Exception {
|
|
final ApiError error;
|
|
ApiException(this.error);
|
|
|
|
@override
|
|
String toString() => error.toString();
|
|
}
|
|
|
|
class ApiClient {
|
|
late final Dio _dio;
|
|
|
|
String _baseUrl;
|
|
|
|
ApiClient({String? baseUrl})
|
|
: _baseUrl = baseUrl ?? AppConstants.defaultBackendUrl {
|
|
_dio = Dio(BaseOptions(
|
|
baseUrl: _baseUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
responseType: ResponseType.json,
|
|
));
|
|
|
|
_dio.interceptors.add(InterceptorsWrapper(
|
|
onError: (error, handler) {
|
|
if (error.response != null) {
|
|
final apiError = ApiError.fromResponse(error.response!);
|
|
handler.reject(DioException(
|
|
requestOptions: error.requestOptions,
|
|
response: error.response,
|
|
error: ApiException(apiError),
|
|
));
|
|
} else {
|
|
handler.next(error);
|
|
}
|
|
},
|
|
));
|
|
}
|
|
|
|
String get baseUrl => _baseUrl;
|
|
|
|
void updateBaseUrl(String newUrl) {
|
|
_baseUrl = newUrl;
|
|
_dio.options.baseUrl = newUrl;
|
|
}
|
|
|
|
/// GET request returning parsed JSON.
|
|
Future<dynamic> get(
|
|
String path, {
|
|
Map<String, dynamic>? queryParameters,
|
|
}) async {
|
|
final response = await _dio.get(path, queryParameters: queryParameters);
|
|
return response.data;
|
|
}
|
|
|
|
/// GET request returning raw Response (for downloads with progress).
|
|
Future<Response> getRaw(
|
|
String path, {
|
|
Map<String, dynamic>? queryParameters,
|
|
ResponseType? responseType,
|
|
void Function(int, int)? onReceiveProgress,
|
|
Options? options,
|
|
}) async {
|
|
return _dio.get(
|
|
path,
|
|
queryParameters: queryParameters,
|
|
options: options ?? Options(responseType: responseType),
|
|
onReceiveProgress: onReceiveProgress,
|
|
);
|
|
}
|
|
|
|
/// The underlying Dio instance, for advanced use cases.
|
|
Dio get dio => _dio;
|
|
}
|
|
|
|
/// Riverpod provider for the API client.
|
|
/// Initialized with the saved backend URL from the database, falling back to
|
|
/// the default if nothing has been saved yet.
|
|
final apiClientProvider = Provider<ApiClient>((ref) {
|
|
return ApiClient();
|
|
});
|
|
|
|
/// Override this in main() after loading the saved URL.
|
|
final initialBackendUrlProvider = Provider<String>((ref) {
|
|
return AppConstants.defaultBackendUrl;
|
|
});
|