Documentation Index
Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Synheart Wear 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.1
Install dependencies:
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.platformHealth, // Apple HealthKit on iOS, 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.platformHealth}),
);
// 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.platformHealth,
}),
);
}
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 (nullable; non-null only when the adapter exposes RR samples)
final rrIntervals = metrics.rrIntervalsMs; // 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.
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 = BleHrmProvider();
// 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.onHeartRate.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:
| Field | Type | Description |
|---|
tsMs | int | Phone receipt timestamp (ms since epoch) |
bpm | double | Heart rate in beats per minute |
source | String | Always "ble_hrm" |
deviceId | String | BLE device UUID |
deviceName | String? | Device advertised name |
sessionId | String? | Optional session tag |
rrIntervalsMs | List<double> | RR intervals in milliseconds (empty when device doesn’t expose RR) |
Error Handling
BLE HRM uses structured error codes returned as PlatformException:
| Code | Meaning |
|---|
PERMISSION_DENIED | Bluetooth permission not granted |
BLUETOOTH_OFF | Bluetooth adapter is disabled |
DEVICE_NOT_FOUND | Device not found or connection timed out |
SUBSCRIBE_FAILED | Failed to subscribe to HR notifications |
DISCONNECTED | Device 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',
);
// 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.
final garminProvider = GarminProvider(
appId: 'your-app-id',
userId: 'user-123',
);
// 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();
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:
| Method | Description | Returns |
|---|
requestPermissions() | Request health data permissions | Future<Map<PermissionType, ConsentStatus>> |
initialize() | Initialize the SDK | Future<void> |
readMetrics() | Read latest metrics | Future<WearMetrics> |
streamHR() | Stream heart rate | Stream<WearMetrics> |
streamHRV() | Stream HRV | Stream<WearMetrics> |
dispose() | Cleanup resources | void |
SynheartWearConfig
Configuration for the SDK.
Constructor:
SynheartWearConfig.withAdapters(
Set<DeviceAdapter> adapters, {
bool enableLocalCaching = true,
bool enableEncryption = true,
})
WearMetrics
Biometric data container.
Methods:
| Method | Description | Returns |
|---|
getMetric(MetricType type) | Get specific metric | double? |
hasValidData | Check if data is valid | bool |
Properties:
| Property | Type | Description |
|---|
timestamp | DateTime | When data was recorded |
deviceId | String | Device identifier |
source | String | Data source |
rrIntervalsMs | List<double>? | RR intervals (ms); non-null only when the adapter exposes them |
batteryLevel | double? | Battery level (0.0-1.0); pulled from meta.battery |
Resources