You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Weight is currently entered manually via a form (lib/widgets/weight/forms.dart). Users with smart scales already have their weight data flowing into Apple Health or Google Health Connect automatically. Requiring them to also manually enter the same data into wger creates friction and reduces engagement.
Proposal
title: "feat: add automatic weight sync from Apple Health and Google Health Connect"
type: feat
date: 2026-03-26
feat: add automatic weight sync from Apple Health and Google Health Connect - Extensive
Overview
Add a health platform integration that automatically imports body weight data from Apple Health (iOS) and Google Health Connect (Android) into wger when the app is opened. Users step on a smart scale, the scale syncs to their phone's health platform, and wger picks up new weight entries on next launch — no manual data entry required.
The integration uses the Flutter health package (v13.3.1) for cross-platform access through a single Dart API. Only weight is implemented, but the architecture supports adding other body measurements later.
Problem Statement
Weight is currently entered manually via a form (lib/widgets/weight/forms.dart). Users with smart scales already have their weight data flowing into Apple Health or Google Health Connect automatically. Requiring them to also manually enter the same data into wger creates friction and reduces engagement.
The wger app has zero health platform integrations today — no health, health_connect, or apple_health packages in pubspec.yaml, no health-related permissions in the Android manifest or iOS Info.plist.
Proposed Solution
A HealthSyncService (Riverpod-based, following the TrophyRepository pattern) that:
Reads weight data from the health platform via the health package
Compares against existing wger entries by calendar date
Imports non-conflicting entries to the wger backend
Presents a conflict resolution dialog for overlapping dates
Persists sync state (enabled flag + last sync timestamp) in SharedPreferences
The feature is opt-in via a settings toggle and runs on app cold start.
Add Health Connect permissions, queries, intent filter
android/app/src/main/kotlin/.../MainActivity.kt
Change to extend FlutterFragmentActivity
ios/Runner/Info.plist
Add NSHealthShareUsageDescription
ios/Runner/Runner.entitlements
Add HealthKit entitlement
lib/helpers/shared_preferences.dart
Add sync preference keys
lib/widgets/core/settings.dart
Add health sync section
lib/screens/home_tabs_screen.dart
Trigger sync after weight entries load
lib/providers/body_weight.dart
Fix findByDate to use calendar-date comparison
lib/models/body_weight/weight_entry.dart
Fix copyWith parameter type (int? → num?)
Pre-requisite: Verify Backend Behavior
Before implementation begins, verify two things against the wger backend API:
Duplicate-date behavior: POST /api/v2/weightentry/ with a date that already has an entry. Does it return 400, overwrite, or create a duplicate?
Storage unit: Does the backend always store weight in kg, or in the user's preferred unit?
These answers determine whether conflict resolution happens purely client-side and whether unit conversion is needed before POSTing.
How to test: Use curl or the wger API browser against a test instance:
# POST first entry
curl -X POST https://wger.example/api/v2/weightentry/ \
-H "Authorization: Token <token>" \
-H "Content-Type: application/json" \
-d '{"date": "2026-03-26", "weight": "80.0"}'# POST duplicate date
curl -X POST https://wger.example/api/v2/weightentry/ \
-H "Authorization: Token <token>" \
-H "Content-Type: application/json" \
-d '{"date": "2026-03-26", "weight": "81.0"}'
Implementation Phases
Phase 1: Foundation — Bug fixes and platform setup
Fix pre-existing bugs and add platform configuration so the health package can function.
Tasks:
Fix WeightEntry.copyWith parameter type from int? to num? (lib/models/body_weight/weight_entry.dart:43)
Fix BodyWeightProvider.findByDate() to compare by calendar date (year/month/day) instead of exact DateTime equality (lib/providers/body_weight.dart:59-61)
Add health: ^13.3.1 to pubspec.yaml
Add Health Connect permissions to android/app/src/main/AndroidManifest.xml:
Intent filter for ACTION_SHOW_PERMISSIONS_RATIONALE
Change MainActivity.kt to extend FlutterFragmentActivity instead of FlutterActivity
Verify android/app/build.gradle has minSdkVersion >= 26 (health package requirement)
Add NSHealthShareUsageDescription to ios/Runner/Info.plist
Add HealthKit capability entitlement to ios/Runner/Runner.entitlements
Add sync-related keys to PreferenceHelper (lib/helpers/shared_preferences.dart):
healthSyncEnabled (bool)
lastHealthSyncTimestamp (String, ISO 8601)
Write tests for the updated findByDate and copyWith
Success criteria: App builds on both platforms with health package included. Existing tests pass. findByDate correctly matches entries by calendar date.
Phase 2: Core sync logic
Implement the health data reading, deduplication, and sync-to-backend flow.
Tasks:
Create HealthSyncService class (lib/providers/health_sync_service.dart):
checkAvailability() — returns whether health platform is available (calls getHealthConnectSdkStatus() on Android, always true on iOS)
readWeightEntries(DateTime since) — reads weight data from health platform since a given date, deduplicates via health.removeDuplicates(), aggregates multiple readings per day to the earliest by timestamp, returns list of (DateTime date, double weightKg) tuples
Create @Riverpod(keepAlive: true) provider for HealthSyncService (lib/providers/health_sync_service.dart), following the TrophyRepository pattern
syncOnAppOpen() — main orchestrator: check if enabled → read health data since last sync → compare against existing wger entries by calendar date → identify conflicts and new entries → import new entries via BodyWeightProvider.addEntry() → return conflicts for UI resolution → update last sync timestamp
enableSync() — request permissions, set preference, trigger initial 30-day sync
disableSync() — clear preference and last sync timestamp
resolveConflicts(Map<DateTime, ConflictDecision> decisions) — apply user decisions (keep health value = editEntry, keep existing = no-op)
Bridge to BodyWeightProvider: The notifier accesses the Provider-based weight provider through the Riverpod wgerBaseProvider for API calls, or directly calls the weight entry API endpoints
Handle unit conversion: The health package returns kg. If backend stores in user's preferred unit and that unit is lb, convert before POSTing. (Depends on pre-requisite verification.)
Handle errors: best-effort sync — save what succeeds, skip what fails, advance last sync timestamp to the latest successfully synced date
Write unit tests for HealthSyncService and HealthSyncNotifier:
Mock the Health class to return known weight data
Test deduplication (multiple readings per day → earliest wins)
Test conflict detection (health entry exists for date with existing wger entry)
Test new entry import (health entry exists for date without wger entry)
Test empty results (no health data)
Test error handling (network failure mid-sync)
Test first-time sync (no last sync timestamp → 30-day lookback)
Test incremental sync (last sync timestamp → only fetch newer data)
Success criteria: Sync logic correctly reads health data, identifies conflicts, imports new entries, and handles errors. All unit tests pass.
Phase 3: Settings UI and conflict resolution dialog
Build the user-facing UI for enabling sync and resolving conflicts.
Shows a scrollable list of conflicting dates with both values (health vs existing)
Each row: date, health value, existing value, radio buttons to pick which to keep
"Apply same decision to all" checkbox at the top
Confirm button applies decisions
Dismiss (back button / tap outside) skips unresolved conflicts — those entries are not imported and will reappear on next sync
Values displayed in user's preferred unit (kg or lb)
Integrate conflict dialog into sync flow in HomeTabsScreen:
After _loadEntries() completes and weight entries are loaded, trigger syncOnAppOpen()
If conflicts are returned, show HealthConflictDialog
Show snackbar for successful imports: "Synced N weight entries from Health" (silent if zero)
Handle iOS silent permission denial: if sync is enabled but first sync returns zero results, show a one-time guidance message suggesting the user check Health permissions in iOS Settings
Success criteria: User can enable/disable sync from settings. Conflicts are shown in a usable dialog. Sync results are communicated via snackbar. Feature is hidden on devices without Health Connect.
Phase 4: Edge cases and polish
Handle remaining edge cases and ensure robustness.
Tasks:
Sync cooldown: add a 15-minute minimum interval between syncs to avoid excessive health API calls on frequent app opens. Store last sync attempt time in memory (not persisted — resets on cold start)
Logout cleanup: clear healthSyncEnabled and lastHealthSyncTimestamp from SharedPreferences when user logs out. Add to AuthProvider.logout() or the existing logout cleanup flow
Offline handling: if the device has no network when sync runs, read health data (local), but skip backend POSTs. Show no error — retry silently on next app open when network is available
Timezone handling: when comparing health platform dates against wger entries, use local time for the calendar-date comparison (consistent with how users think about "today's weight")
Precision handling: round health platform weight values to 2 decimal places before comparison and storage, matching the wger backend's "99.00" format
Integration test: full flow from enabling sync to seeing imported entries on the weight screen
Update any existing weight tests affected by the findByDate change
Success criteria: All edge cases handled gracefully. No data loss or duplicates. Feature works correctly across app restarts, logout/login, and network changes.
Alternative Approaches Considered
Approach
Why Rejected
Native platform channels (Swift + Kotlin)
Significantly more code to maintain across two languages. No benefit over the health package for this use case.
Server-side integration
Apple Health and Health Connect are on-device APIs — no server-side access exists.
Background sync
Adds complexity (WorkManager, BGTaskScheduler, background permissions) for marginal benefit. On-app-open is sufficient for daily weigh-in patterns.
Bidirectional sync
Write permissions complicate App Store review and risk infinite-loop sync. Deferred to v2.
Acceptance Criteria
Functional Requirements
User can enable health sync from a settings toggle
On app open (with sync enabled), new weight entries from Apple Health / Health Connect are automatically imported to wger
When a health entry conflicts with an existing wger entry (same calendar date), a dialog asks the user which to keep
The conflict dialog includes an "apply same decision to all" option
Multiple readings per day are aggregated to the earliest reading
The initial sync imports the last 30 days of data
Subsequent syncs only check for data since the last successful sync
On Android, the feature is hidden when Health Connect is not available
Disabling sync clears the sync preference and last sync timestamp
Logging out clears sync state
Non-Functional Requirements
Sync does not block app startup — runs after weight entries are loaded, errors are non-fatal
Sync completes within 5 seconds for typical usage (1-30 new entries)
Health data permissions are requested only when the user explicitly enables sync
No health data is read or stored until the user opts in
Weight values are stored with correct units (matching backend expectations)
Quality Gates
Unit tests for HealthSyncService and HealthSyncNotifier with mocked Health class
Widget tests for settings tile and conflict dialog
Pre-existing findByDate and copyWith bug fixes have regression tests
Use case
Weight is currently entered manually via a form (
lib/widgets/weight/forms.dart). Users with smart scales already have their weight data flowing into Apple Health or Google Health Connect automatically. Requiring them to also manually enter the same data into wger creates friction and reduces engagement.Proposal
title: "feat: add automatic weight sync from Apple Health and Google Health Connect"
type: feat
date: 2026-03-26
feat: add automatic weight sync from Apple Health and Google Health Connect - Extensive
Overview
Add a health platform integration that automatically imports body weight data from Apple Health (iOS) and Google Health Connect (Android) into wger when the app is opened. Users step on a smart scale, the scale syncs to their phone's health platform, and wger picks up new weight entries on next launch — no manual data entry required.
The integration uses the Flutter
healthpackage (v13.3.1) for cross-platform access through a single Dart API. Only weight is implemented, but the architecture supports adding other body measurements later.Problem Statement
Weight is currently entered manually via a form (
lib/widgets/weight/forms.dart). Users with smart scales already have their weight data flowing into Apple Health or Google Health Connect automatically. Requiring them to also manually enter the same data into wger creates friction and reduces engagement.The wger app has zero health platform integrations today — no
health,health_connect, orapple_healthpackages inpubspec.yaml, no health-related permissions in the Android manifest or iOS Info.plist.Proposed Solution
A
HealthSyncService(Riverpod-based, following theTrophyRepositorypattern) that:healthpackageThe feature is opt-in via a settings toggle and runs on app cold start.
Technical Approach
Architecture
Key files to create:
lib/providers/health_sync_service.dartlib/providers/health_sync_notifier.dartlib/widgets/core/settings/health_sync.dartlib/widgets/weight/health_conflict_dialog.dartKey files to modify:
pubspec.yamlhealth: ^13.3.1dependencyandroid/app/src/main/AndroidManifest.xmlandroid/app/src/main/kotlin/.../MainActivity.ktFlutterFragmentActivityios/Runner/Info.plistNSHealthShareUsageDescriptionios/Runner/Runner.entitlementslib/helpers/shared_preferences.dartlib/widgets/core/settings.dartlib/screens/home_tabs_screen.dartlib/providers/body_weight.dartfindByDateto use calendar-date comparisonlib/models/body_weight/weight_entry.dartcopyWithparameter type (int?→num?)Pre-requisite: Verify Backend Behavior
Before implementation begins, verify two things against the wger backend API:
POST /api/v2/weightentry/with a date that already has an entry. Does it return 400, overwrite, or create a duplicate?These answers determine whether conflict resolution happens purely client-side and whether unit conversion is needed before POSTing.
How to test: Use
curlor the wger API browser against a test instance:Implementation Phases
Phase 1: Foundation — Bug fixes and platform setup
Fix pre-existing bugs and add platform configuration so the health package can function.
Tasks:
WeightEntry.copyWithparameter type fromint?tonum?(lib/models/body_weight/weight_entry.dart:43)BodyWeightProvider.findByDate()to compare by calendar date (year/month/day) instead of exactDateTimeequality (lib/providers/body_weight.dart:59-61)health: ^13.3.1topubspec.yamlandroid/app/src/main/AndroidManifest.xml:<uses-permission android:name="android.permission.health.READ_WEIGHT"/><queries>block for Health Connect packageViewPermissionUsageActivityactivity-aliasACTION_SHOW_PERMISSIONS_RATIONALEMainActivity.ktto extendFlutterFragmentActivityinstead ofFlutterActivityandroid/app/build.gradlehasminSdkVersion >= 26(health package requirement)NSHealthShareUsageDescriptiontoios/Runner/Info.plistios/Runner/Runner.entitlementsPreferenceHelper(lib/helpers/shared_preferences.dart):healthSyncEnabled(bool)lastHealthSyncTimestamp(String, ISO 8601)findByDateandcopyWithSuccess criteria: App builds on both platforms with health package included. Existing tests pass.
findByDatecorrectly matches entries by calendar date.Phase 2: Core sync logic
Implement the health data reading, deduplication, and sync-to-backend flow.
Tasks:
HealthSyncServiceclass (lib/providers/health_sync_service.dart):checkAvailability()— returns whether health platform is available (callsgetHealthConnectSdkStatus()on Android, always true on iOS)requestPermissions()— requestsHealthDataType.WEIGHTread permissionreadWeightEntries(DateTime since)— reads weight data from health platform since a given date, deduplicates viahealth.removeDuplicates(), aggregates multiple readings per day to the earliest by timestamp, returns list of(DateTime date, double weightKg)tuples@Riverpod(keepAlive: true)provider forHealthSyncService(lib/providers/health_sync_service.dart), following theTrophyRepositorypatternHealthSyncNotifier(lib/providers/health_sync_notifier.dart):syncOnAppOpen()— main orchestrator: check if enabled → read health data since last sync → compare against existing wger entries by calendar date → identify conflicts and new entries → import new entries viaBodyWeightProvider.addEntry()→ return conflicts for UI resolution → update last sync timestampenableSync()— request permissions, set preference, trigger initial 30-day syncdisableSync()— clear preference and last sync timestampresolveConflicts(Map<DateTime, ConflictDecision> decisions)— apply user decisions (keep health value =editEntry, keep existing = no-op)BodyWeightProvider: The notifier accesses the Provider-based weight provider through the RiverpodwgerBaseProviderfor API calls, or directly calls the weight entry API endpointshealthpackage returns kg. If backend stores in user's preferred unit and that unit is lb, convert before POSTing. (Depends on pre-requisite verification.)HealthSyncServiceandHealthSyncNotifier:Healthclass to return known weight dataSuccess criteria: Sync logic correctly reads health data, identifies conflicts, imports new entries, and handles errors. All unit tests pass.
Phase 3: Settings UI and conflict resolution dialog
Build the user-facing UI for enabling sync and resolving conflicts.
Tasks:
HealthSyncSettingsTilewidget (lib/widgets/core/settings/health_sync.dart):ListTilewith a toggle switch to enable/disable health synccheckAvailability())lib/widgets/core/settings.dart) in a new "Health" sectionHealthConflictDialogwidget (lib/widgets/weight/health_conflict_dialog.dart):HomeTabsScreen:_loadEntries()completes and weight entries are loaded, triggersyncOnAppOpen()HealthConflictDialogSuccess criteria: User can enable/disable sync from settings. Conflicts are shown in a usable dialog. Sync results are communicated via snackbar. Feature is hidden on devices without Health Connect.
Phase 4: Edge cases and polish
Handle remaining edge cases and ensure robustness.
Tasks:
healthSyncEnabledandlastHealthSyncTimestampfrom SharedPreferences when user logs out. Add toAuthProvider.logout()or the existing logout cleanup flow"99.00"formatfindByDatechangeSuccess criteria: All edge cases handled gracefully. No data loss or duplicates. Feature works correctly across app restarts, logout/login, and network changes.
Alternative Approaches Considered
healthpackage for this use case.Acceptance Criteria
Functional Requirements
Non-Functional Requirements
Quality Gates
HealthSyncServiceandHealthSyncNotifierwith mockedHealthclassfindByDateandcopyWithbug fixes have regression testsflutter analyze)TrophyRepository/TrophyStateNotifier)Success Metrics
Dependencies & Prerequisites
healthpackage v13.3.1pubspec.yamlminSdkVersion >= 26flutter.minSdkVersion)Risk Analysis & Mitigation
minSdkVersionincrease drops Android usershealthpackage breaking changes^13.3.1to stay within minor versionmain.dart(wgerBaseProvideroverride)Future Considerations
HealthSyncServicearchitecture supports this via additionalHealthDataTypevalueshealthpackage's background delivery support on iOS and WorkManager on Androidsourcefield onWeightEntryand backend API supportDocumentation Plan
README.mdwith health sync feature description and platform setup requirementsReferences & Research
Internal References
lib/models/body_weight/weight_entry.dartlib/providers/body_weight.dartlib/screens/home_tabs_screen.dart:84-150(_loadEntries)lib/providers/trophies.dart(TrophyRepository + TrophyStateNotifier)lib/providers/wger_base_riverpod.dartandlib/main.dart:229-231lib/widgets/core/settings.dartlib/helpers/shared_preferences.dartandroid/app/src/main/AndroidManifest.xmlios/Runner/Info.plistwingspan/brainstorms/2026-03-26-auto-weight-sync-brainstorm-doc.mdExternal References
healthpackage: https://pub.dev/packages/healthhealthpackage source: https://github.com/carp-dk/carp-health-flutterAdditional Context
No response