Updates & http client enhancements

pull/303/head
haroon amjad 5 hours ago
parent 34cc16c24f
commit c9406aa66d

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api/http_client_manager.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -13,6 +14,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:http/http.dart' as http;
@ -87,10 +89,14 @@ abstract class ApiClient {
class ApiClientImp implements ApiClient {
final _analytics = getIt<GAnalytics>();
final AppState _appState;
final HttpClientManager _httpClient;
ApiClientImp({
required AppState appState,
}) : _appState = appState;
HttpClientManager? httpClient,
}) : _appState = appState,
_httpClient = httpClient ??
HttpClientManager(lifecycleService: getIt<AppLifecycleService>());
@override
post(
@ -242,7 +248,11 @@ class ApiClientImp implements ApiClient {
http.Response response;
try {
response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers);
response = await _httpClient.post(
uri: Uri.parse(url.trim()),
body: requestBody,
headers: headers,
);
// debugPrint("response: ${response.body}", wrapWidth: 2048);
} on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
@ -445,8 +455,8 @@ class ApiClientImp implements ApiClient {
if (await Utils.checkConnection(bypassConnectionCheck: true)) {
http.Response response;
try {
response = await http.get(
Uri.parse(url.trim()),
response = await _httpClient.get(
uri: Uri.parse(url.trim()),
headers: apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'},
);
} on SocketException catch (e) {

@ -0,0 +1,224 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:http/http.dart' as http;
/// HTTP Client Manager with automatic retry logic for transient network errors
/// Handles background/foreground transitions gracefully
class HttpClientManager {
final http.Client _client;
final AppLifecycleService _lifecycleService;
static const Duration _defaultTimeout = Duration(seconds: 30);
static const int _maxRetries = 2;
HttpClientManager({
required AppLifecycleService lifecycleService,
http.Client? client,
}) : _lifecycleService = lifecycleService,
_client = client ?? http.Client();
/// Execute POST request with retry logic
Future<http.Response> post({
required Uri uri,
Map<String, String>? headers,
Object? body,
Duration? timeout,
int maxRetries = _maxRetries,
}) async {
return _executeWithRetry(
() => _client
.post(uri, headers: headers, body: body)
.timeout(timeout ?? _defaultTimeout),
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(),
maxRetries: maxRetries,
);
}
/// Execute GET request with retry logic
Future<http.Response> get({
required Uri uri,
Map<String, String>? headers,
Duration? timeout,
int maxRetries = _maxRetries,
}) async {
return _executeWithRetry(
() => _client
.get(uri, headers: headers)
.timeout(timeout ?? _defaultTimeout),
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(),
maxRetries: maxRetries,
);
}
/// Core retry logic for HTTP requests
Future<http.Response> _executeWithRetry(
Future<http.Response> Function() request, {
required String endpoint,
int maxRetries = _maxRetries,
}) async {
int attempt = 0;
while (attempt <= maxRetries) {
try {
// Wait for app to be in foreground before attempting request
await _waitForAppForeground();
// Check if app just came from background
if (_lifecycleService.lastResumedTime != null && attempt == 0) {
final timeSinceResume = DateTime.now().difference(_lifecycleService.lastResumedTime!);
if (timeSinceResume.inSeconds < 5) {
debugPrint('🔄 App recently resumed (${timeSinceResume.inSeconds}s ago), adding small delay before request...');
await Future.delayed(const Duration(milliseconds: 500));
}
}
return await request();
} on SocketException catch (e) {
if (attempt >= maxRetries) {
// If app is in background, wait for it to come back before throwing
if (_lifecycleService.isAppInBackground) {
debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint');
await _waitForAppForeground();
}
rethrow;
}
await _handleRetry(
attempt: attempt++,
errorType: 'SocketException',
endpoint: endpoint,
errorDetails: e.message,
);
} on http.ClientException catch (e) {
if (attempt >= maxRetries) {
// If app is in background, wait for it to come back before throwing
if (_lifecycleService.isAppInBackground) {
debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint');
await _waitForAppForeground();
}
rethrow;
}
await _handleRetry(
attempt: attempt++,
errorType: 'ClientException',
endpoint: endpoint,
errorDetails: e.message,
);
} on TimeoutException catch (e) {
// For timeout, only retry once
if (attempt >= 1) {
// If app is in background, wait for it to come back before throwing
if (_lifecycleService.isAppInBackground) {
debugPrint('⏸️ App in background, waiting to resume before final error for $endpoint');
await _waitForAppForeground();
}
rethrow;
}
await _handleRetry(
attempt: attempt++,
errorType: 'TimeoutException',
endpoint: endpoint,
errorDetails: e.message ?? 'Request timed out',
);
}
}
throw Exception('Max retries exceeded for $endpoint');
}
/// Wait for app to be in foreground before proceeding
Future<void> _waitForAppForeground() async {
if (!_lifecycleService.isAppInBackground) {
return; // App is already in foreground
}
debugPrint('⏸️ App is in background, pausing request until app resumes...');
// Wait for app to resume with a timeout
final completer = Completer<void>();
late StreamSubscription<AppLifecycleState> subscription;
// Set up a listener for app state changes
subscription = _lifecycleService.appStateStream.listen((state) {
if (state == AppLifecycleState.resumed) {
if (!completer.isCompleted) {
debugPrint('✅ App resumed, continuing with request');
completer.complete();
}
}
});
// Also check current state in case it changed
if (_lifecycleService.currentState == AppLifecycleState.resumed) {
if (!completer.isCompleted) {
completer.complete();
}
}
try {
// Wait for app to resume with 60 second timeout
await completer.future.timeout(
const Duration(seconds: 60),
onTimeout: () {
debugPrint('⚠️ Timeout waiting for app to resume');
},
);
} finally {
await subscription.cancel();
}
// Add small delay after resume to let things stabilize
await Future.delayed(const Duration(milliseconds: 300));
}
/// Handle retry delay with exponential backoff and validation
Future<void> _handleRetry({
required int attempt,
required String errorType,
required String endpoint,
required String errorDetails,
}) async {
// If app went to background during the error, wait for it to come back
if (_lifecycleService.isAppInBackground) {
debugPrint('⏸️ Error occurred while app in background, waiting for app to resume...');
await _waitForAppForeground();
}
// Check network connectivity before retrying
final hasConnection = await Utils.checkConnection(bypassConnectionCheck: true);
if (!hasConnection) {
debugPrint('⚠️ No network connection available, waiting before retry...');
// Wait a bit and check again instead of immediately throwing
await Future.delayed(const Duration(seconds: 2));
final recheckConnection = await Utils.checkConnection(bypassConnectionCheck: true);
if (!recheckConnection) {
throw SocketException('No network connection available for retry');
}
}
// Exponential backoff: 300ms, 900ms, 2700ms
final delay = Duration(milliseconds: 300 * (1 << attempt));
debugPrint(
'🔄 Retry attempt ${attempt + 1}/$_maxRetries for $endpoint\n'
' Error: $errorType - $errorDetails\n'
' Waiting: ${delay.inMilliseconds}ms\n'
' App State: ${_lifecycleService.currentState}'
);
await Future.delayed(delay);
}
/// Close the HTTP client
void dispose() {
_client.close();
}
}

@ -68,6 +68,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_repo.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -140,6 +141,9 @@ class AppDependencies {
loggerService: getIt(),
));
// App Lifecycle Service - must be registered before ApiClient
getIt.registerLazySingleton<AppLifecycleService>(() => AppLifecycleService());
getIt.registerLazySingleton<ApiClient>(() => ApiClientImp(appState: getIt()));
getIt.registerLazySingleton<LocalAuthService>(
() => LocalAuthService(loggerService: getIt<LoggerService>(), localAuth: getIt<LocalAuthentication>()),

@ -49,6 +49,8 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_mo
import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/app_theme.dart';
@ -123,6 +125,9 @@ Future<void> callInitializations() async {
HttpOverrides.global = MyHttpOverrides();
await callAppStateInitializations();
// Initialize App Lifecycle Service to monitor background/foreground transitions
getIt.get<AppLifecycleService>().initialize();
// Restore persisted dark-mode preference before the first frame.
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
}

@ -20,8 +20,9 @@ class LabResultList extends StatelessWidget {
selector: (_, model) => model.mainLabResultsByHospitals,
builder: (__, list, ___) {
if (list.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
return Utils.getNoDataWidget(context,
noDataText: LocaleKeys.noLabResults.tr(context: context));
// return Utils.getNoDataWidget(context,
// noDataText: LocaleKeys.noLabResults.tr(context: context));
return Container();
} else {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// Service to monitor and track app lifecycle state changes
/// This helps detect when app goes to background/foreground
class AppLifecycleService with WidgetsBindingObserver {
final _stateController = StreamController<AppLifecycleState>.broadcast();
/// Stream of app lifecycle state changes
Stream<AppLifecycleState> get appStateStream => _stateController.stream;
AppLifecycleState _currentState = AppLifecycleState.resumed;
/// Current app lifecycle state
AppLifecycleState get currentState => _currentState;
/// Check if app is currently active/visible
bool get isAppActive => _currentState == AppLifecycleState.resumed;
/// Check if app is in background or inactive
bool get isAppInBackground =>
_currentState == AppLifecycleState.paused ||
_currentState == AppLifecycleState.inactive ||
_currentState == AppLifecycleState.detached;
/// Last time app was resumed
DateTime? _lastResumedTime;
DateTime? get lastResumedTime => _lastResumedTime;
/// Last time app went to background
DateTime? _lastPausedTime;
DateTime? get lastPausedTime => _lastPausedTime;
/// Duration app has been in current state
Duration get timeInCurrentState {
final referenceTime = _currentState == AppLifecycleState.resumed
? _lastResumedTime
: _lastPausedTime;
if (referenceTime == null) return Duration.zero;
return DateTime.now().difference(referenceTime);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_currentState = state;
// Track timing
if (state == AppLifecycleState.resumed) {
_lastResumedTime = DateTime.now();
debugPrint('📱 App RESUMED');
} else if (state == AppLifecycleState.paused) {
_lastPausedTime = DateTime.now();
debugPrint('📱 App PAUSED/BACKGROUNDED');
} else if (state == AppLifecycleState.inactive) {
debugPrint('📱 App INACTIVE');
} else if (state == AppLifecycleState.detached) {
debugPrint('📱 App DETACHED');
}
_stateController.add(state);
}
/// Initialize the lifecycle observer
void initialize() {
WidgetsBinding.instance.addObserver(this);
debugPrint('📱 AppLifecycleService initialized');
}
/// Clean up resources
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_stateController.close();
debugPrint('📱 AppLifecycleService disposed');
}
}
Loading…
Cancel
Save