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.2.3
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

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

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(),
);

// Various Garmin data types
final dailies = await garminProvider.fetchDailies(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final epochs = await garminProvider.fetchEpochs(
  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 stress = await garminProvider.fetchStressDetails(
  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(),
);
// Additional methods: fetchUserMetrics(), fetchBodyComps(), 
// fetchPulseOx(), fetchRespiration(), fetchHealthSnapshot(),
// fetchBloodPressures(), fetchSkinTemp()

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