Skip to content

import weight data from Health Connect #1153

@JohnWeidner

Description

@JohnWeidner

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 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:

  1. Reads weight data from the health platform via the health package
  2. Compares against existing wger entries by calendar date
  3. Imports non-conflicting entries to the wger backend
  4. Presents a conflict resolution dialog for overlapping dates
  5. 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.

Technical Approach

Architecture

┌─────────────────────────────────────────────────┐
│                   UI Layer                       │
│  SettingsHealthSyncTile  │  ConflictDialog       │
└──────────┬───────────────┴──────────┬────────────┘
           │                          │
┌──────────▼──────────────────────────▼────────────┐
│              State Layer (Riverpod)               │
│  HealthSyncNotifier (@Riverpod keepAlive: true)   │
│  - syncOnAppOpen()                                │
│  - enableSync() / disableSync()                   │
│  - resolveConflicts(decisions)                    │
└──────────┬──────────────────────────┬────────────┘
           │                          │
┌──────────▼──────────┐  ┌───────────▼────────────┐
│  HealthSyncService   │  │  BodyWeightProvider    │
│  (health package)    │  │  (existing, Provider)  │
│  - readWeight()      │  │  - addEntry()          │
│  - checkAvailable()  │  │  - editEntry()         │
│  - requestPerms()    │  │  - findByDate()        │
└──────────────────────┘  └────────────────────────┘

Key files to create:

File Purpose
lib/providers/health_sync_service.dart Health platform read logic + Riverpod provider
lib/providers/health_sync_notifier.dart State management, sync orchestration
lib/widgets/core/settings/health_sync.dart Settings toggle UI
lib/widgets/weight/health_conflict_dialog.dart Conflict resolution dialog

Key files to modify:

File Change
pubspec.yaml Add health: ^13.3.1 dependency
android/app/src/main/AndroidManifest.xml 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:

  1. Duplicate-date behavior: POST /api/v2/weightentry/ with a date that already has an entry. Does it return 400, overwrite, or create a duplicate?
  2. 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:
    • <uses-permission android:name="android.permission.health.READ_WEIGHT"/>
    • <queries> block for Health Connect package
    • ViewPermissionUsageActivity activity-alias
    • 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)
    • requestPermissions() — requests HealthDataType.WEIGHT read permission
    • 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
  • Create HealthSyncNotifier (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 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.

Tasks:

  • Create HealthSyncSettingsTile widget (lib/widgets/core/settings/health_sync.dart):
    • A ListTile with a toggle switch to enable/disable health sync
    • On Android: hidden entirely if Health Connect is not available (checkAvailability())
    • On enable: triggers permission request → initial sync → shows result snackbar
    • On disable: clears sync state
    • Shows last sync timestamp below the toggle when enabled
  • Add the health sync tile to the settings page (lib/widgets/core/settings.dart) in a new "Health" section
  • Create HealthConflictDialog widget (lib/widgets/weight/health_conflict_dialog.dart):
    • 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
  • Write widget tests:
    • Settings tile: toggle on/off, availability check, permission request flow
    • Conflict dialog: single conflict, multiple conflicts, "apply to all", dismiss behavior
    • Snackbar feedback: success message, error message, zero-entry silence

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
  • All existing weight tests continue to pass
  • Static analysis clean (flutter analyze)
  • Code follows existing Riverpod patterns (matches TrophyRepository / TrophyStateNotifier)

Success Metrics

  • Users with sync enabled no longer need to manually enter weight
  • No duplicate weight entries created by the sync process
  • Conflict resolution dialog is used successfully (not dismissed/abandoned at high rates)

Dependencies & Prerequisites

Dependency Status Impact
health package v13.3.1 Available on pub.dev Must be added to pubspec.yaml
Android minSdkVersion >= 26 Needs verification (currently uses flutter.minSdkVersion) If current min is < 26, raising it may drop support for older devices
Backend duplicate-date behavior Unknown — must verify before Phase 2 Determines sync write strategy (POST vs PATCH)
Backend storage unit Unknown — must verify before Phase 2 Determines whether unit conversion is needed
iOS HealthKit capability Requires Xcode project change Cannot test on simulator — requires physical device

Risk Analysis & Mitigation

Risk Likelihood Impact Mitigation
Backend rejects duplicate-date POSTs Medium High — sync fails silently Verify before implementation. If rejects, use PATCH for conflicts, POST for new entries
iOS silent permission denial confuses users High Medium — sync appears broken Show guidance message when sync is enabled but returns zero results
minSdkVersion increase drops Android users Low Medium Check current resolved value. Health Connect is bundled on Android 14+; for earlier versions, prompt installation
health package breaking changes Low High Pin to ^13.3.1 to stay within minor version
Provider↔Riverpod bridge adds complexity Medium Low Follow established bridge pattern from main.dart (wgerBaseProvider override)

Future Considerations

  • Bidirectional sync (write wger entries to health platform) — requires write permissions and loop prevention
  • Additional measurement types (body fat %, BMI, blood pressure) — the HealthSyncService architecture supports this via additional HealthDataType values
  • Background sync — could be added later using the health package's background delivery support on iOS and WorkManager on Android
  • Visual distinction for synced entries — would require a source field on WeightEntry and backend API support
  • Configurable aggregation — let user choose first/last/average reading per day
  • Configurable initial sync range — let user choose 7/30/90 days on first enable

Documentation Plan

  • Update README.md with health sync feature description and platform setup requirements
  • Add user-facing documentation for enabling sync (could be in-app or on wger.readthedocs.io)
  • Document the required iOS entitlement and Android manifest changes for contributors

References & Research

Internal References

  • Weight model: lib/models/body_weight/weight_entry.dart
  • Weight provider: lib/providers/body_weight.dart
  • App initialization: lib/screens/home_tabs_screen.dart:84-150 (_loadEntries)
  • Riverpod pattern to follow: lib/providers/trophies.dart (TrophyRepository + TrophyStateNotifier)
  • Riverpod bridge: lib/providers/wger_base_riverpod.dart and lib/main.dart:229-231
  • Settings page: lib/widgets/core/settings.dart
  • Preferences: lib/helpers/shared_preferences.dart
  • Android manifest: android/app/src/main/AndroidManifest.xml
  • iOS Info.plist: ios/Runner/Info.plist
  • Brainstorm: wingspan/brainstorms/2026-03-26-auto-weight-sync-brainstorm-doc.md

External References

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions