networking and error handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:provider/single_child_widget.dart';
|
||||
import 'package:tm_app/core/config/network_manager.dart';
|
||||
import 'package:tm_app/core/theme/theme_provider.dart';
|
||||
import 'package:tm_app/data/repositories/home_repository.dart';
|
||||
import 'package:tm_app/data/repositories/tender_detail_repository.dart';
|
||||
@@ -14,7 +15,6 @@ import 'package:tm_app/view_models/your_tenders_view_model.dart';
|
||||
|
||||
import '../../data/repositories/auth_repository.dart';
|
||||
import '../../data/services/auth_service.dart';
|
||||
import '../../data/services/network_manager.dart';
|
||||
import '../../data/services/tenders_service.dart';
|
||||
import '../../view_models/auth_view_model.dart';
|
||||
import '../../view_models/home_view_model.dart';
|
||||
@@ -87,9 +87,6 @@ List<SingleChildWidget> get viewModels {
|
||||
(context) =>
|
||||
YourTendersViewModel(yourTendersRepository: context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => TendersViewModel(tendersRepository: context.read()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/app_exceptions.dart';
|
||||
import '../utils/logger.dart';
|
||||
import '../utils/network_connectivity.dart';
|
||||
import '../utils/result.dart';
|
||||
|
||||
class NetworkManager {
|
||||
final Dio mainDio;
|
||||
final NetworkConnectivity _connectivity = NetworkConnectivity();
|
||||
|
||||
NetworkManager()
|
||||
: mainDio = Dio(
|
||||
BaseOptions(
|
||||
// baseUrl: 'https://stage.cura-bau.de',
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
sendTimeout: const Duration(seconds: 10),
|
||||
contentType: 'application/json',
|
||||
headers: {'Accept': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> init() async {
|
||||
await addInterceptors();
|
||||
}
|
||||
|
||||
Future<void> addInterceptors() async {
|
||||
final requestInterceptor = InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
appLogger.info('🌐 Request: ${options.method} ${options.path}');
|
||||
appLogger.info('🌐 Headers: ${options.headers}');
|
||||
return handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
appLogger.info(
|
||||
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
|
||||
);
|
||||
appLogger.info('✅ Response data: ${response.data}');
|
||||
return handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
appLogger.error(
|
||||
'❌ Error: ${error.message} - ${error.requestOptions.path}',
|
||||
);
|
||||
appLogger.error('❌ Error type: ${error.type}');
|
||||
return handler.next(error);
|
||||
},
|
||||
);
|
||||
|
||||
mainDio.interceptors.add(requestInterceptor);
|
||||
}
|
||||
|
||||
/// Enhanced makeRequest method with comprehensive error handling
|
||||
Future<Result<T>> makeRequest<T>(
|
||||
String endpoint,
|
||||
T Function(Map<String, dynamic>) fromJson, {
|
||||
String method = 'GET',
|
||||
FormData? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
appLogger.info('🚀 network request: $method $endpoint');
|
||||
|
||||
try {
|
||||
// Check network connectivity first
|
||||
if (!await _connectivity.hasInternetConnection()) {
|
||||
appLogger.error('❌ No internet connection available');
|
||||
return Result.error(
|
||||
NetworkConnectionException('No internet connection available'),
|
||||
);
|
||||
}
|
||||
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
Dio dioInstance = mainDio;
|
||||
|
||||
await addInterceptors();
|
||||
|
||||
Future<Response> makeApiCall(String token, String url) async {
|
||||
final headers =
|
||||
token.isEmpty
|
||||
? <String, String>{}
|
||||
: {'Authorization': 'Bearer $token'};
|
||||
|
||||
final options = Options(
|
||||
headers: headers,
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
);
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case 'POST':
|
||||
return await dioInstance.post(
|
||||
url + endpoint,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
case 'PUT':
|
||||
return await dioInstance.put(
|
||||
url + endpoint,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
case 'PATCH':
|
||||
return await dioInstance.patch(
|
||||
url + endpoint,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
case 'DELETE':
|
||||
return await dioInstance.delete(
|
||||
url + endpoint,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
case 'GET':
|
||||
return await dioInstance.get(
|
||||
url + endpoint,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
default:
|
||||
return await dioInstance.get(
|
||||
url + endpoint,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? bearerToken = prefs.getString('bearer');
|
||||
String? url = prefs.getString('url');
|
||||
|
||||
appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
|
||||
appLogger.info(
|
||||
'🔑 Bearer token: ${bearerToken != null ? 'PRESENT' : 'NOT SET'}',
|
||||
);
|
||||
|
||||
if (url == null || url.isEmpty) {
|
||||
appLogger.error('❌ Base URL not configured');
|
||||
return Result.error(BadRequestException('Base URL not configured'));
|
||||
}
|
||||
|
||||
Response res = await makeApiCall(bearerToken ?? '', url);
|
||||
|
||||
// Handle successful responses
|
||||
if (res.statusCode == 200 || res.statusCode == 201) {
|
||||
appLogger.info('✅ Request successful: ${res.statusCode}');
|
||||
try {
|
||||
final result = fromJson(res.data);
|
||||
appLogger.info('✅ Response parsed successfully');
|
||||
return Result.ok(result);
|
||||
} on Exception catch (e) {
|
||||
appLogger.error('❌ Failed to parse response: $e');
|
||||
return Result.error(
|
||||
FetchDataException(
|
||||
'Failed to parse response data',
|
||||
res.statusCode,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle specific HTTP error codes
|
||||
return _handleHttpError(res);
|
||||
} on DioException catch (e) {
|
||||
return _handleDioException(e);
|
||||
} on Exception catch (e) {
|
||||
appLogger.error('Unexpected error in makeRequest: $e');
|
||||
return Result.error(
|
||||
FetchDataException('Unexpected error occurred', null, e),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle HTTP error responses
|
||||
Result<T> _handleHttpError<T>(Response response) {
|
||||
final statusCode = response.statusCode;
|
||||
final message = _extractErrorMessage(response.data);
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return Result.error(
|
||||
BadRequestException(message, statusCode, response.data),
|
||||
);
|
||||
case 401:
|
||||
return Result.error(
|
||||
AuthenticationException(message, statusCode, response.data),
|
||||
);
|
||||
case 403:
|
||||
return Result.error(
|
||||
ForbiddenException(message, statusCode, response.data),
|
||||
);
|
||||
case 404:
|
||||
return Result.error(
|
||||
NotFoundException(message, statusCode, response.data),
|
||||
);
|
||||
case 423:
|
||||
return Result.error(
|
||||
ForbiddenException(
|
||||
'Resource is locked: $message',
|
||||
statusCode,
|
||||
response.data,
|
||||
),
|
||||
);
|
||||
case 429:
|
||||
return Result.error(
|
||||
RateLimitException(message, statusCode, response.data),
|
||||
);
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return Result.error(
|
||||
ServerException(message, statusCode, response.data),
|
||||
);
|
||||
default:
|
||||
return Result.error(
|
||||
FetchDataException(message, statusCode, response.data),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Dio exceptions
|
||||
Result<T> _handleDioException<T>(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return Result.error(TimeoutException('Request timed out', e));
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return Result.error(
|
||||
NetworkConnectionException('Connection error occurred', e),
|
||||
);
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
if (e.response?.statusCode == 401) {
|
||||
// Handle token refresh logic here if needed
|
||||
// For now, return authentication error
|
||||
return Result.error(
|
||||
AuthenticationException(
|
||||
'Authentication failed',
|
||||
e.response?.statusCode,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Result.error(
|
||||
FetchDataException(
|
||||
'Bad response from server',
|
||||
e.response?.statusCode,
|
||||
e,
|
||||
),
|
||||
);
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return Result.error(
|
||||
FetchDataException('Request was cancelled', null, e),
|
||||
);
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
if (e.error is SocketException) {
|
||||
return Result.error(
|
||||
NetworkConnectionException('No internet connection', e),
|
||||
);
|
||||
}
|
||||
return Result.error(
|
||||
FetchDataException('Network error occurred', null, e),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract error message from response data
|
||||
String _extractErrorMessage(data) {
|
||||
if (data == null) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data['message'] ??
|
||||
data['error'] ??
|
||||
data['detail'] ??
|
||||
'Unknown error';
|
||||
}
|
||||
|
||||
if (data is String) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
/// Simple request method for backward compatibility
|
||||
// Future<T> makeSimpleRequest<T>(
|
||||
// String endpoint,
|
||||
// T Function(Map<String, dynamic>) fromJson, {
|
||||
// String method = 'GET',
|
||||
// FormData? data,
|
||||
// }) async {
|
||||
// final result = await makeRequest(
|
||||
// endpoint,
|
||||
// fromJson,
|
||||
// method: method,
|
||||
// data: data,
|
||||
// );
|
||||
|
||||
// return switch (result) {
|
||||
// Ok(value: final value) => value,
|
||||
// Error(error: final error) => throw error,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user