Merge branch 'master' into dev_aamir
commit
c93aa5993e
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
class ScheduleAppointmentRequestModel {
|
||||
final String generalId;
|
||||
final String fileNo;
|
||||
final String appointmentNo;
|
||||
final String doctorId;
|
||||
final String appointmentDate;
|
||||
final String mobileNumber;
|
||||
final int projectId;
|
||||
final int clinicId;
|
||||
|
||||
ScheduleAppointmentRequestModel({
|
||||
required this.generalId,
|
||||
required this.fileNo,
|
||||
required this.appointmentNo,
|
||||
required this.doctorId,
|
||||
required this.appointmentDate,
|
||||
required this.mobileNumber,
|
||||
required this.projectId,
|
||||
required this.clinicId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'generalId': generalId,
|
||||
'fileNo': fileNo,
|
||||
'appointmentNo': appointmentNo,
|
||||
'doctorId': doctorId,
|
||||
'appointmentDate': appointmentDate,
|
||||
'mobileNumber': mobileNumber,
|
||||
'projectId': projectId,
|
||||
'clinicId': clinicId,
|
||||
};
|
||||
}
|
||||
|
||||
factory ScheduleAppointmentRequestModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScheduleAppointmentRequestModel(
|
||||
generalId: json['generalId'] ?? '',
|
||||
fileNo: json['fileNo'] ?? '',
|
||||
appointmentNo: json['appointmentNo'] ?? '',
|
||||
doctorId: json['doctorId'] ?? '',
|
||||
appointmentDate: json['appointmentDate'] ?? '',
|
||||
mobileNumber: json['mobileNumber'] ?? '',
|
||||
projectId: json['projectId'] ?? 0,
|
||||
clinicId: json['clinicId'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
class ScheduleAppointmentResponseModel {
|
||||
final bool? success;
|
||||
final String? message;
|
||||
final String? appointmentId;
|
||||
final dynamic data;
|
||||
|
||||
ScheduleAppointmentResponseModel({
|
||||
this.success,
|
||||
this.message,
|
||||
this.appointmentId,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory ScheduleAppointmentResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScheduleAppointmentResponseModel(
|
||||
success: json['success'],
|
||||
message: json['message'],
|
||||
appointmentId: json['appointmentId'],
|
||||
data: json['data'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'appointmentId': appointmentId,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,252 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/smartwatches/activity_detail.dart';
|
||||
import 'package:hmg_patient_app_new/services/navigation_service.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hmg_patient_app_new/features/smartwatch_health_data/HealthDataTransformation.dart' as durations;
|
||||
|
||||
import '../../core/utils/date_util.dart' show DateUtil;
|
||||
|
||||
class SmartWatchActivity extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: CollapsingListView(
|
||||
title: "All Health Data".needTranslation,
|
||||
child: Column(
|
||||
spacing: 16.h,
|
||||
children: [
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchActivity,
|
||||
title: "Activity Calories".needTranslation,
|
||||
description: "Activity rings give you a quick visual reference of how active you are each day. ".needTranslation,
|
||||
trailingIcon: AppAssets.watchActivityTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.activity??[]),
|
||||
unitsOfMeasure: "Kcal"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("activity");
|
||||
context.read<HealthProvider>().saveSelectedSection("activity");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("activity", sectionName:"Activity Calories", uom: "Kcal");
|
||||
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSteps,
|
||||
title: "Steps".needTranslation,
|
||||
description: "Step count is the number of steps you take throughout the day.".needTranslation,
|
||||
trailingIcon: AppAssets.watchStepsTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.step??[]),
|
||||
unitsOfMeasure: "Steps"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("steps");
|
||||
context.read<HealthProvider>().saveSelectedSection("steps");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("steps", sectionName: "Steps", uom: "Steps");
|
||||
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSteps,
|
||||
title: "Distance Covered".needTranslation,
|
||||
description: "Step count is the distance you take throughout the day.".needTranslation,
|
||||
trailingIcon: AppAssets.watchStepsTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.distance??[]),
|
||||
unitsOfMeasure: "Km"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("distance");
|
||||
context.read<HealthProvider>().saveSelectedSection("distance");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("distance", sectionName: "Distance Covered", uom: "km");
|
||||
|
||||
}),
|
||||
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSleep,
|
||||
title: "Sleep Score".needTranslation,
|
||||
description: "This will keep track of how much hours you sleep in a day".needTranslation,
|
||||
trailingIcon: AppAssets.watchSleepTrailing,
|
||||
result: DateUtil.millisToHourMin(int.parse(context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.sleep??[]))).split(" ")[0],
|
||||
unitsOfMeasure: "hr",
|
||||
resultSecondValue: DateUtil.millisToHourMin(int.parse(context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.sleep??[]))).split(" ")[2],
|
||||
unitOfSecondMeasure: "min"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("sleep");
|
||||
context.read<HealthProvider>().saveSelectedSection("sleep");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("sleep", sectionName:"Sleep Score",uom:"");
|
||||
|
||||
}),
|
||||
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchWeight,
|
||||
title: "Blood Oxygen".needTranslation,
|
||||
description: "This will calculate your Blood Oxygen to keep track and update history".needTranslation,
|
||||
trailingIcon: AppAssets.watchWeightTrailing,
|
||||
result: context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.bodyOxygen??[], ),
|
||||
unitsOfMeasure: "%"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("bodyOxygen");
|
||||
context.read<HealthProvider>().saveSelectedSection("bodyOxygen");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("bodyOxygen", uom: "%", sectionName:"Blood Oxygen" );
|
||||
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchWeight,
|
||||
title: "Body temperature".needTranslation,
|
||||
description: "This will calculate your Body temprerature to keep track and update history".needTranslation,
|
||||
trailingIcon: AppAssets.watchWeightTrailing,
|
||||
result: context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.bodyTemperature??[]),
|
||||
unitsOfMeasure: "C"
|
||||
).onPress((){
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("bodyTemperature");
|
||||
context.read<HealthProvider>().saveSelectedSection("bodyTemperature");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("bodyTemperature" , sectionName: "Body temperature".capitalizeFirstofEach, uom: "C");
|
||||
|
||||
}),
|
||||
],
|
||||
).paddingSymmetrical(24.w, 24.h),
|
||||
));
|
||||
}
|
||||
|
||||
Widget resultItem({
|
||||
required String leadingIcon,
|
||||
required String title,
|
||||
required String description,
|
||||
required String trailingIcon,
|
||||
required String result,
|
||||
required String unitsOfMeasure,
|
||||
String? resultSecondValue,
|
||||
String? unitOfSecondMeasure
|
||||
}) {
|
||||
return DecoratedBox(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h),
|
||||
child: Row(
|
||||
spacing: 16.w,
|
||||
children: [
|
||||
Expanded(
|
||||
child:Column(
|
||||
spacing: 8.h,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.w,
|
||||
children: [
|
||||
Utils.buildSvgWithAssets(icon: leadingIcon, height: 16.h, width: 14.w),
|
||||
title.toText16( weight: FontWeight.w600, color: AppColors.textColor),
|
||||
],
|
||||
),
|
||||
description.toText12(isBold: true, color: AppColors.greyTextColor),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
spacing: 2.h,
|
||||
children: [
|
||||
result.toText21(isBold: true, color: AppColors.textColor),
|
||||
unitsOfMeasure.toText10(isBold: true, color:AppColors.greyTextColor ),
|
||||
if(resultSecondValue != null)
|
||||
Visibility(
|
||||
visible: resultSecondValue != null ,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
spacing: 2.h,
|
||||
children: [
|
||||
SizedBox(width: 2.w,),
|
||||
resultSecondValue.toText21(isBold: true, color: AppColors.textColor),
|
||||
unitOfSecondMeasure!.toText10(isBold: true, color:AppColors.greyTextColor )
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
],
|
||||
) ,
|
||||
),
|
||||
Utils.buildSvgWithAssets(icon: trailingIcon, width: 72.w, height: 72.h),
|
||||
],
|
||||
).paddingSymmetrical(16.w, 16.h)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_export.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_data_transformations.dart' as durations;
|
||||
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/utils/date_util.dart' show DateUtil;
|
||||
|
||||
class SmartWatchesHealthDataScreen extends StatelessWidget {
|
||||
const SmartWatchesHealthDataScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.bgScaffoldColor,
|
||||
body: CollapsingListView(
|
||||
title: "All Health Data".needTranslation,
|
||||
child: Column(
|
||||
spacing: 16.h,
|
||||
children: [
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchActivity,
|
||||
title: "Activity Calories".needTranslation,
|
||||
description: "Activity rings give you a quick visual reference of how active you are each day. ".needTranslation,
|
||||
trailingIcon: AppAssets.watchActivityTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.activity ?? []),
|
||||
unitsOfMeasure: "Kcal")
|
||||
.onPress(() {
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("activity");
|
||||
context.read<HealthProvider>().saveSelectedSection("activity");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("activity", sectionName: "Activity Calories", uom: "Kcal");
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSteps,
|
||||
title: "Steps".needTranslation,
|
||||
description: "Step count is the number of steps you take throughout the day.".needTranslation,
|
||||
trailingIcon: AppAssets.watchStepsTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.step ?? []),
|
||||
unitsOfMeasure: "Steps")
|
||||
.onPress(() {
|
||||
// Map<String, List<Vitals>> getVitals() {
|
||||
// return {
|
||||
// "heartRate": heartRate ,
|
||||
// "sleep": sleep,
|
||||
// "steps": step,
|
||||
// "activity": activity,
|
||||
// "bodyOxygen": bodyOxygen,
|
||||
// "bodyTemperature": bodyTemperature,
|
||||
// };
|
||||
// }
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("steps");
|
||||
context.read<HealthProvider>().saveSelectedSection("steps");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("steps", sectionName: "Steps", uom: "Steps");
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSteps,
|
||||
title: "Distance Covered".needTranslation,
|
||||
description: "Step count is the distance you take throughout the day.".needTranslation,
|
||||
trailingIcon: AppAssets.watchStepsTrailing,
|
||||
result: context.read<HealthProvider>().sumOfNonEmptyData(context.read<HealthProvider>().vitals?.distance ?? []),
|
||||
unitsOfMeasure: "Km")
|
||||
.onPress(() {
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("distance");
|
||||
context.read<HealthProvider>().saveSelectedSection("distance");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("distance", sectionName: "Distance Covered", uom: "km");
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchSleep,
|
||||
title: "Sleep Score".needTranslation,
|
||||
description: "This will keep track of how much hours you sleep in a day".needTranslation,
|
||||
trailingIcon: AppAssets.watchSleepTrailing,
|
||||
result: DateUtil.millisToHourMin(
|
||||
int.parse(context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.sleep ?? [])))
|
||||
.split(" ")[0],
|
||||
unitsOfMeasure: "hr",
|
||||
resultSecondValue: DateUtil.millisToHourMin(
|
||||
int.parse(context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.sleep ?? [])))
|
||||
.split(" ")[2],
|
||||
unitOfSecondMeasure: "min")
|
||||
.onPress(() {
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("sleep");
|
||||
context.read<HealthProvider>().saveSelectedSection("sleep");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("sleep", sectionName: "Sleep Score", uom: "");
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchWeight,
|
||||
title: "Blood Oxygen".needTranslation,
|
||||
description: "This will calculate your Blood Oxygen to keep track and update history".needTranslation,
|
||||
trailingIcon: AppAssets.watchWeightTrailing,
|
||||
result: context.read<HealthProvider>().firstNonEmptyValue(
|
||||
context.read<HealthProvider>().vitals?.bodyOxygen ?? [],
|
||||
),
|
||||
unitsOfMeasure: "%")
|
||||
.onPress(() {
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("bodyOxygen");
|
||||
context.read<HealthProvider>().saveSelectedSection("bodyOxygen");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("bodyOxygen", uom: "%", sectionName: "Blood Oxygen");
|
||||
}),
|
||||
resultItem(
|
||||
leadingIcon: AppAssets.watchWeight,
|
||||
title: "Body temperature".needTranslation,
|
||||
description: "This will calculate your Body temprerature to keep track and update history".needTranslation,
|
||||
trailingIcon: AppAssets.watchWeightTrailing,
|
||||
result: context.read<HealthProvider>().firstNonEmptyValue(context.read<HealthProvider>().vitals?.bodyTemperature ?? []),
|
||||
unitsOfMeasure: "C")
|
||||
.onPress(() {
|
||||
context.read<HealthProvider>().setDurations(durations.Durations.daily);
|
||||
context.read<HealthProvider>().deleteDataIfSectionIsDifferent("bodyTemperature");
|
||||
context.read<HealthProvider>().saveSelectedSection("bodyTemperature");
|
||||
context.read<HealthProvider>().fetchData();
|
||||
context.read<HealthProvider>().navigateToDetails("bodyTemperature", sectionName: "Body temperature".capitalizeFirstofEach, uom: "C");
|
||||
}),
|
||||
],
|
||||
).paddingSymmetrical(24.w, 24.h),
|
||||
));
|
||||
}
|
||||
|
||||
Widget resultItem({
|
||||
required String leadingIcon,
|
||||
required String title,
|
||||
required String description,
|
||||
required String trailingIcon,
|
||||
required String result,
|
||||
required String unitsOfMeasure,
|
||||
String? resultSecondValue,
|
||||
String? unitOfSecondMeasure,
|
||||
}) {
|
||||
return DecoratedBox(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h),
|
||||
child: Row(
|
||||
spacing: 16.w,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 8.h,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.w,
|
||||
children: [
|
||||
Utils.buildSvgWithAssets(icon: leadingIcon, height: 16.h, width: 14.w),
|
||||
title.toText16(weight: FontWeight.w600, color: AppColors.textColor),
|
||||
],
|
||||
),
|
||||
description.toText12(isBold: true, color: AppColors.greyTextColor),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
spacing: 2.h,
|
||||
children: [
|
||||
result.toText21(isBold: true, color: AppColors.textColor),
|
||||
unitsOfMeasure.toText10(isBold: true, color: AppColors.greyTextColor),
|
||||
if (resultSecondValue != null)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
spacing: 2.h,
|
||||
children: [
|
||||
SizedBox(width: 2.w),
|
||||
resultSecondValue.toText21(isBold: true, color: AppColors.textColor),
|
||||
unitOfSecondMeasure!.toText10(isBold: true, color: AppColors.greyTextColor)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Utils.buildSvgWithAssets(icon: trailingIcon, width: 72.w, height: 72.h),
|
||||
],
|
||||
).paddingSymmetrical(16.w, 16.h));
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue