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

Initialize the SDK

import 'package:synheart_wear/synheart_wear.dart';

final synheart = SynheartWear(
  config: SynheartWearConfig.withAdapters({
    DeviceAdapter.appleHealthKit, // Uses Health Connect on Android
  }),
);

Request Permissions

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

if (result[PermissionType.heartRate] == ConsentStatus.granted) {
  print('Heart rate permission granted!');
}

Initialize and Read Metrics

await synheart.initialize();

final metrics = await synheart.readMetrics();
print('HR: ${metrics.getMetric(MetricType.hr)} bpm');
print('Steps: ${metrics.getMetric(MetricType.steps)}');
print('HRV RMSSD: ${metrics.getMetric(MetricType.hrvRmssd)} ms');

Real-Time Streaming

Stream Heart Rate

synheart.streamHR(interval: Duration(seconds: 5))
  .listen((metrics) {
    final hr = metrics.getMetric(MetricType.hr);
    if (hr != null) {
      print('Current HR: $hr bpm');
    }
  });

Stream HRV

synheart.streamHRV(windowSize: Duration(seconds: 60))
  .listen((metrics) {
    final hrvRmssd = metrics.getMetric(MetricType.hrvRmssd);
    final hrvSdnn = metrics.getMetric(MetricType.hrvSdnn);

    if (hrvRmssd != null) {
      print('HRV RMSSD: $hrvRmssd ms');
    }
  });

Stream with Error Handling

synheart.streamHR(interval: Duration(seconds: 3))
  .listen(
    (metrics) {
      // Handle metrics
      final hr = metrics.getMetric(MetricType.hr);
      print('HR: $hr bpm');
    },
    onError: (error) {
      if (error is PermissionDeniedError) {
        print('Permission denied');
      } else if (error is DeviceUnavailableError) {
        print('Device not available');
      } else {
        print('Error: $error');
      }
    },
    onDone: () {
      print('Stream completed');
    },
  );

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 {
      final result = await _sdk.requestPermissions(
        permissions: {
          PermissionType.heartRate,
          PermissionType.steps,
          PermissionType.calories,
        },
        reason: 'Track your health metrics',
      );

      if (result.values.any((s) => s == ConsentStatus.granted)) {
        await _sdk.initialize();
        final metrics = await _sdk.readMetrics();
        setState(() {
          _isConnected = true;
          _latestMetrics = metrics;
        });
      }
    } catch (e) {
      print('Error: $e');
    }
  }

  void _startStreaming() {
    _hrSubscription = _sdk
        .streamHR(interval: Duration(seconds: 3))
        .listen((metrics) {
      setState(() => _latestMetrics = metrics);
    });
  }

  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 {
  final metrics = await synheart.readMetrics();
  if (metrics.hasValidData) {
    print('Data available');
  }
} on PermissionDeniedError catch (e) {
  print('Permission denied: ${e.message}');
  // Prompt user to grant permission
} on DeviceUnavailableError catch (e) {
  print('Device unavailable: ${e.message}');
  // Show device connection instructions
} on SynheartWearError catch (e) {
  print('SDK error: ${e.message}');
  // General error handling
}

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