Skip to main content

Overview

The Synheart Wear Dart/Flutter SDK provides a unified API for streaming biometric data from Apple Watch, Fitbit, Garmin, Whoop, and Samsung devices in Flutter applications. Key Features:
  • Cross-platform support (iOS + Android)
  • Real-time HR and HRV streaming
  • Unified data schema across all devices
  • Encrypted local storage
  • Consent-based permissions

Installation

Add to your pubspec.yaml:
dependencies:
  synheart_wear: ^0.3.0
Install dependencies:
flutter pub get

Platform Configuration

iOS Configuration

Add to ios/Runner/Info.plist:
<key>NSHealthShareUsageDescription</key>
<string>This app needs access to your health data to provide insights.</string>

<key>NSHealthUpdateUsageDescription</key>
<string>This app needs permission to update your health data.</string>

Android Configuration

Add to android/app/src/main/AndroidManifest.xml:
<!-- Health Connect Permissions -->
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
<uses-permission android:name="android.permission.health.READ_DISTANCE"/>

<!-- Health Connect Package Query -->
<queries>
    <package android:name="com.google.android.apps.healthdata" />
    <intent>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
    </intent>
</queries>

<application>
    <!-- Required: Privacy Policy Activity Alias -->
    <activity-alias
        android:name="ViewPermissionUsageActivity"
        android:exported="true"
        android:targetActivity=".MainActivity"
        android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
        <intent-filter>
            <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
            <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
        </intent-filter>
    </activity-alias>
</application>
Important: Your MainActivity must extend FlutterFragmentActivity (not FlutterActivity) for Android 14+.

Basic Usage

Recommended Pattern (Explicit Permission Control):
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:synheart_wear/synheart_wear.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Step 1: Create SDK instance
  final adapters = <DeviceAdapter>{
    DeviceAdapter.appleHealthKit, // Uses Health Connect on Android
  };

  // Use withAdapters() to explicitly specify which adapters to enable
  // Note: Default constructor includes fitbit by default, so use withAdapters() for clarity
  final synheart = SynheartWear(
    config: SynheartWearConfig.withAdapters(adapters),
  );

  // Step 2: Request permissions (with reason for better UX)
  final result = await synheart.requestPermissions(
    permissions: {
      PermissionType.heartRate,
      PermissionType.steps,
      PermissionType.calories,
    },
    reason: 'This app needs access to your health data.',
  );

  // Step 3: Initialize SDK (validates permissions and data availability)
  if (result.values.any((s) => s == ConsentStatus.granted)) {
    try {
      await synheart.initialize();
      
      // Step 4: Read metrics
      final metrics = await synheart.readMetrics();
      print('HR: ${metrics.getMetric(MetricType.hr)} bpm');
      print('Steps: ${metrics.getMetric(MetricType.steps)}');
    } on SynheartWearError catch (e) {
      print('Initialization failed: $e');
      // Handle errors: NO_WEARABLE_DATA, STALE_DATA, etc.
    }
  }
}
Alternative Pattern (Simplified): If you don’t need to provide a custom reason, you can let initialize() handle permissions automatically:
final synheart = SynheartWear(
  config: SynheartWearConfig.withAdapters({DeviceAdapter.appleHealthKit}),
);

// Initialize will request permissions internally if needed
await synheart.initialize();
final metrics = await synheart.readMetrics();
Note: initialize() validates that wearable data is available and not stale (>24 hours old). If no data is available or data is too old, it will throw a SynheartWearError with codes NO_WEARABLE_DATA or STALE_DATA.

Real-Time Streaming

// Stream heart rate every 5 seconds
// Note: Streams are created lazily when first listener subscribes
// Multiple calls to streamHR() return the same stream controller
final hrSubscription = synheart.streamHR(interval: Duration(seconds: 5))
  .listen((metrics) {
    final hr = metrics.getMetric(MetricType.hr);
    if (hr != null) print('Current HR: $hr bpm');
  }, onError: (error) {
    print('Stream error: $error');
  });

// Stream HRV in 5-second windows
final hrvSubscription = synheart.streamHRV(windowSize: Duration(seconds: 5))
  .listen((metrics) {
    final hrv = metrics.getMetric(MetricType.hrvRmssd);
    if (hrv != null) print('HRV RMSSD: $hrv ms');
  }, onError: (error) {
    print('HRV stream error: $error');
  });

// Don't forget to cancel subscriptions when done
// hrSubscription.cancel();
// hrvSubscription.cancel();

Complete Example

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:synheart_wear/synheart_wear.dart';

class HealthMonitor extends StatefulWidget {
  @override
  _HealthMonitorState createState() => _HealthMonitorState();
}

class _HealthMonitorState extends State<HealthMonitor> {
  late SynheartWear _sdk;
  StreamSubscription<WearMetrics>? _hrSubscription;
  WearMetrics? _latestMetrics;
  bool _isConnected = false;

  @override
  void initState() {
    super.initState();
    _sdk = SynheartWear(
      config: SynheartWearConfig.withAdapters({
        DeviceAdapter.appleHealthKit,
      }),
    );
  }

  Future<void> _connect() async {
    try {
      // Step 1: Request permissions
      final result = await _sdk.requestPermissions(
        permissions: {
          PermissionType.heartRate,
          PermissionType.steps,
          PermissionType.calories,
        },
        reason: 'This app needs access to your health data.',
      );

      // Step 2: Initialize if permissions granted
      if (result.values.any((s) => s == ConsentStatus.granted)) {
        await _sdk.initialize();
        
        // Step 3: Read initial metrics
        final metrics = await _sdk.readMetrics();
        setState(() {
          _isConnected = true;
          _latestMetrics = metrics;
        });
      } else {
        setState(() {
          _isConnected = false;
          // Show error: permissions denied
        });
      }
    } on SynheartWearError catch (e) {
      // Handle SDK-specific errors (NO_WEARABLE_DATA, STALE_DATA, etc.)
      print('SDK Error: $e');
      setState(() {
        _isConnected = false;
        // Show error message
      });
    } catch (e) {
      // Handle other errors
      print('Error: $e');
      setState(() {
        _isConnected = false;
      });
    }
  }

  void _startStreaming() {
    _hrSubscription = _sdk
        .streamHR(interval: Duration(seconds: 3))
        .listen(
      (metrics) {
        setState(() => _latestMetrics = metrics);
      },
      onError: (error) {
        print('Stream error: $error');
        // Handle stream errors
      },
    );
  }

  void _stopStreaming() {
    _hrSubscription?.cancel();
    _hrSubscription = null;
  }

  @override
  void dispose() {
    _hrSubscription?.cancel();
    _sdk.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Health Monitor')),
      body: _isConnected
          ? Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                if (_latestMetrics != null) ...[
                  Text(
                    'HR: ${_latestMetrics!.getMetric(MetricType.hr) ?? "--"} bpm',
                    style: TextStyle(fontSize: 32),
                  ),
                  SizedBox(height: 16),
                  Text('Steps: ${_latestMetrics!.getMetric(MetricType.steps) ?? "--"}'),
                  Text('Calories: ${_latestMetrics!.getMetric(MetricType.calories) ?? "--"} kcal'),
                  SizedBox(height: 24),
                ],
                ElevatedButton(
                  onPressed: _hrSubscription == null ? _startStreaming : _stopStreaming,
                  child: Text(_hrSubscription == null ? 'Start Streaming' : 'Stop Streaming'),
                ),
              ],
            )
          : Center(
              child: ElevatedButton(
                onPressed: _connect,
                child: Text('Connect to Health'),
              ),
            ),
    );
  }
}

Data Schema

Access metrics using the WearMetrics object:
final metrics = await synheart.readMetrics();

// Basic metrics
final hr = metrics.getMetric(MetricType.hr);           // Heart rate (bpm)
final steps = metrics.getMetric(MetricType.steps);     // Steps
final calories = metrics.getMetric(MetricType.calories); // Calories (kcal)
final distance = metrics.getMetric(MetricType.distance); // Distance (km)

// HRV metrics
final hrvRmssd = metrics.getMetric(MetricType.hrvRmssd); // HRV RMSSD (ms)
final hrvSdnn = metrics.getMetric(MetricType.hrvSdnn);   // HRV SDNN (ms)

// RR intervals
final rrIntervals = metrics.rrIntervals; // List<double>

// Metadata
final batteryLevel = metrics.batteryLevel;     // 0.0-1.0
final deviceId = metrics.deviceId;             // String
final source = metrics.source;                 // String
final timestamp = metrics.timestamp;           // DateTime

Error Handling

try {
  // Request permissions
  final result = await synheart.requestPermissions(
    permissions: {PermissionType.heartRate, PermissionType.steps},
    reason: 'This app needs access to your health data.',
  );

  if (result.values.any((s) => s == ConsentStatus.granted)) {
    // Initialize (may throw if no data or stale data)
    await synheart.initialize();
    
    // Read metrics
    final metrics = await synheart.readMetrics();
    if (metrics.hasValidData) {
      print('Data available');
    }
  }
} on PermissionDeniedError catch (e) {
  print('Permission denied: ${e.message}');
  // User denied permissions - show message or retry
} on DeviceUnavailableError catch (e) {
  print('Device unavailable: ${e.message}');
  // Health data source not available - check device connection
} on SynheartWearError catch (e) {
  // Handle SDK-specific errors
  if (e.code == 'NO_WEARABLE_DATA') {
    print('No wearable data available. Please check device connection.');
  } else if (e.code == 'STALE_DATA') {
    print('Data is stale. Please sync your wearable device.');
  } else {
    print('SDK error: ${e.message}');
  }
} catch (e) {
  print('Unexpected error: $e');
}
Common Error Codes:
  • NO_WEARABLE_DATA: No health data available from connected devices
  • STALE_DATA: Latest data is older than 24 hours
  • PERMISSION_DENIED: User denied required permissions
  • DEVICE_UNAVAILABLE: Health data source is not available

BLE Heart Rate Monitor

Connect directly to any standard Bluetooth LE heart rate monitor (WHOOP Broadcast, Polar, Wahoo, Garmin HRM straps) for real-time HR streaming. No cloud backend needed.

Platform Setup

Android — Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
iOS — Add to Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to heart rate monitors.</string>

Usage

import 'package:synheart_wear/synheart_wear.dart';

// Create the BLE HRM bridge
final bleHrm = BleHrmBridge();

// Scan for nearby HR monitors (optionally filter by name prefix)
final devices = await bleHrm.scan(timeoutMs: 10000, namePrefix: 'WHOOP');

// Connect to a device
await bleHrm.connect(
  deviceId: devices.first.deviceId,
  sessionId: 'my-session',
);

// Listen to heart rate samples
bleHrm.heartRateStream.listen((sample) {
  print('BPM: ${sample.bpm}');
  if (sample.rrIntervalsMs != null) {
    print('RR intervals: ${sample.rrIntervalsMs}');
  }
});

// Check connection status
final connected = await bleHrm.isConnected();

// Disconnect when done
await bleHrm.disconnect();

HeartRateSample

Each sample from the stream contains:
FieldTypeDescription
tsMsintPhone receipt timestamp (ms since epoch)
bpmintHeart rate in beats per minute
sourceStringAlways "ble_hrm"
deviceIdStringBLE device UUID
deviceNameString?Device advertised name
sessionIdString?Optional session tag
rrIntervalsMsList<double>?RR intervals in milliseconds (device-dependent)

Error Handling

BLE HRM uses structured error codes returned as PlatformException:
CodeMeaning
PERMISSION_DENIEDBluetooth permission not granted
BLUETOOTH_OFFBluetooth adapter is disabled
DEVICE_NOT_FOUNDDevice not found or connection timed out
SUBSCRIBE_FAILEDFailed to subscribe to HR notifications
DISCONNECTEDDevice disconnected (emitted on event stream)

Notes

  • WHOOP devices require “Broadcast Heart Rate” enabled in the WHOOP app settings
  • RR intervals are available on some devices (e.g. Polar chest straps) but not all
  • Reconnection is automatic: 3 retries with exponential backoff (1s, 2s, 4s)
  • Only one BLE HRM device can be connected at a time

Cloud Providers

WhoopProvider

final whoopProvider = WhoopProvider(
  appId: 'your-app-id',
  userId: 'user-123',
);

// Fetch raw data for Flux processing
final rawData = await whoopProvider.fetchRawDataForFlux(
  start: DateTime.now().subtract(Duration(days: 30)),
  end: DateTime.now(),
);

// Historical data methods
final recovery = await whoopProvider.fetchRecovery(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final sleep = await whoopProvider.fetchSleep(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final workouts = await whoopProvider.fetchWorkouts(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final cycles = await whoopProvider.fetchCycles(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);

GarminProvider (Cloud)

Full Garmin Connect integration via OAuth PKCE. Supports 12 summary types through the Garmin Health API backfill endpoint: dailies, epochs, sleeps, stressDetails, hrv, userMetrics, bodyComps, pulseox, respiration, healthSnapshot, bloodPressures, and skinTemp. Also supports Flux processing for HSI 1.0 compliant output.
final garminProvider = GarminProvider(
  appId: 'your-app-id',
  userId: 'user-123',
);

// Fetch raw data for Flux processing
final rawData = await garminProvider.fetchRawDataForFlux(
  start: DateTime.now().subtract(Duration(days: 30)),
  end: DateTime.now(),
);

// Backfill API — fetch any of the 12 summary types
final dailies = await garminProvider.fetchDailies(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final sleeps = await garminProvider.fetchSleeps(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final hrv = await garminProvider.fetchHRV(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final stress = await garminProvider.fetchStressDetails(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);

// Additional methods: fetchEpochs(), fetchUserMetrics(), fetchBodyComps(),
// fetchPulseOx(), fetchRespiration(), fetchHealthSnapshot(),
// fetchBloodPressures(), fetchSkinTemp()

GarminHealth (Native RTS)

The GarminHealth facade provides native Garmin device integration for scanning, pairing, and real-time streaming. All method signatures use generic SDK-owned types (ScannedDevice, PairedDevice, WearMetrics), never Garmin-specific types.
Important: The Garmin Health SDK Real-Time Streaming (RTS) capability requires a separate license from Garmin. This facade is available on demand for licensed integrations. The underlying Garmin Health SDK code is proprietary to Garmin and is not distributed as open source.
final garmin = GarminHealth(licenseKey: 'your-garmin-sdk-key');
await garmin.initialize();

// Scan for devices
garmin.scannedDevicesStream.listen((devices) {
  print('Found ${devices.length} devices');
});
await garmin.startScanning();

// Pair with a device
final paired = await garmin.pairDevice(scannedDevice);

// Stream real-time metrics
garmin.realTimeStream.listen((metrics) {
  final hr = metrics.getMetric(MetricType.hr);
  print('HR: $hr bpm');
});
await garmin.startStreaming(device: paired);

// Read unified metrics
final metrics = await garmin.readMetrics();

Flux Integration

Flux is a data processing pipeline that converts vendor-specific data into HSI 1.0 compliant format. Native Flux binaries are automatically bundled for pub.dev users (no setup required).
import 'dart:convert';
import 'package:synheart_wear/synheart_wear.dart';

// Enable Flux in config
final synheart = SynheartWear(
  config: SynheartWearConfig(enableFlux: true),
);

// Step 1: Fetch raw data from WHOOP or Garmin
final whoopProvider = WhoopProvider(
  appId: 'your-app-id',
  userId: 'user-123',
);
final rawData = await whoopProvider.fetchRawDataForFlux(
  start: DateTime.now().subtract(Duration(days: 30)),
  end: DateTime.now(),
);
final rawJson = jsonEncode(rawData);

// Step 2: Process with Flux to get HSI 1.0 compliant data
final hsiData = await synheart.readFluxSnapshot(
  vendor: Vendor.whoop,
  rawJson: rawJson,
);

// HSI output includes:
// - Sleep data organized by daily windows
// - Physiology metrics (HR, HRV, etc.)
// - Activity data
// All in HSI 1.0 compliant format
Note: Flux automatically removes UUIDs, ensures required fields, and calculates missing values to produce HSI-compliant output.

Platform-Specific Notes

Android

  • HRV: Only HRV_RMSSD is supported by Health Connect
  • Distance: Uses DISTANCE_DELTA type
  • Requires Health Connect app installed
  • MainActivity must extend FlutterFragmentActivity

iOS

  • Full support for all metrics via HealthKit
  • Requires HealthKit capability enabled in Xcode
  • Background delivery available for real-time updates

API Reference

SynheartWear

Main SDK class for interacting with wearable devices. Constructor:
SynheartWear({required SynheartWearConfig config})
Methods:
MethodDescriptionReturns
requestPermissions()Request health data permissionsFuture<Map<PermissionType, ConsentStatus>>
initialize()Initialize the SDKFuture<void>
readMetrics()Read latest metricsFuture<WearMetrics>
streamHR()Stream heart rateStream<WearMetrics>
streamHRV()Stream HRVStream<WearMetrics>
readFluxSnapshot()Process vendor data through Flux pipelineFuture<Map<String, dynamic>>
dispose()Cleanup resourcesvoid

SynheartWearConfig

Configuration for the SDK. Constructor:
SynheartWearConfig.withAdapters(
  Set<DeviceAdapter> adapters, {
  bool enableLocalCaching = true,
  bool enableEncryption = true,
})

WearMetrics

Biometric data container. Methods:
MethodDescriptionReturns
getMetric(MetricType type)Get specific metricdouble?
hasValidDataCheck if data is validbool
Properties:
PropertyTypeDescription
timestampDateTimeWhen data was recorded
deviceIdStringDevice identifier
sourceStringData source
rrIntervalsList<double>RR intervals (ms)
batteryLeveldouble?Battery level (0.0-1.0)

Resources


Author: Israel Goytom