Platform Differences
While react-native-healthx provides a unified API, there are important differences between iOS HealthKit and Android Health Connect that you should be aware of.
Feature Comparison
| Feature | iOS (HealthKit) | Android (Health Connect) |
|---|---|---|
| Read sleep data | ✅ Yes | ✅ Yes |
| Write sleep data | ❌ No | ✅ Yes |
| Delete sleep data | ❌ No | ✅ Yes |
| Sleep stages | ✅ Yes (iOS 16+) | ✅ Yes |
| Native aggregation | ❌ No (JS fallback) | ✅ Yes |
| Background sync | ❌ Not yet | ❌ Not yet |
Sleep Stage Mapping
Different platforms use different sleep stage classifications. react-native-healthx normalizes these to a unified set:
| Unified Type | iOS HealthKit | Android Health Connect |
|---|---|---|
UNKNOWN | UNKNOWN | UNKNOWN (0) |
AWAKE | AWAKE | AWAKE (1) |
IN_BED | INBED | - (not available) |
SLEEPING | ASLEEP | SLEEPING (2) |
LIGHT | CORE | LIGHT (4) |
DEEP | DEEP | DEEP (5) |
REM | REM | REM (6) |
OUT_OF_BED | - (not available) | OUT_OF_BED (3) |
iOS Sleep Stages
On iOS 15 and earlier, only basic sleep stages are available:
IN_BED- User marked as in bedSLEEPING- Generic asleep stateAWAKE- Awake during sleep period
iOS 16+ adds detailed stages:
LIGHT(called "Core" in HealthKit)DEEPREM
Android Sleep Stages
Health Connect provides all detailed stages from the start, including OUT_OF_BED which iOS doesn't track.
Write Limitations
iOS
iOS HealthKit does not allow third-party apps to write sleep analysis data. This is an Apple limitation, not a library limitation.
// This will throw on iOS
try {
await HealthX.sleep.writeSession({ ... });
} catch (error) {
console.log(error.message);
// "Writing sleep data is not supported on iOS..."
}
Always check before writing:
if (HealthX.supportsWrite()) {
await HealthX.sleep.writeSession({ ... });
}
Android
Full read/write support is available on Android with Health Connect.
Aggregation Implementation
iOS
On iOS, aggregation is calculated in JavaScript by fetching all samples and computing statistics client-side. This means:
- All data must be fetched from HealthKit first
- Large date ranges may be slower
- Stage breakdown is calculated from individual samples
Android
Health Connect has native aggregation support, which is more efficient:
- Uses
aggregateRecord()API - Only returns computed results, not raw data
- Better performance for large datasets
Permission Behavior
iOS
- Permission dialog shows read/write options
- Users can grant partial permissions
- You cannot check if read permission was denied - iOS doesn't expose this
- Permission changes require the user to go to the Health app
Android
- Granular permission dialog with checkboxes
- Clear indication of granted/denied status
- Users manage permissions in Health Connect app
- Revoked permissions take effect immediately
Data Source Tracking
iOS
Sleep data includes source information:
sourceName: Human-readable name (e.g., "Apple Watch")sourceId: Bundle identifier (e.g., "com.apple.health")
Android
Health Connect tracks data origins:
dataOrigin: Package name of the app that wrote the data
Handling Platform Differences
Conditional Features
function SleepTracker() {
const canWrite = HealthX.supportsWrite();
const platform = HealthX.getPlatform();
return (
<View>
<SleepHistory />
{canWrite && (
<Button
title="Log Sleep Manually"
onPress={handleManualLog}
/>
)}
{platform === 'ios' && (
<Text>
Tip: Use Apple Watch for automatic sleep tracking
</Text>
)}
</View>
);
}
Platform-Specific Messaging
function getHealthAppName() {
return HealthX.getPlatform() === 'ios'
? 'Health app'
: 'Health Connect app';
}
function showPermissionDeniedMessage() {
const appName = getHealthAppName();
Alert.alert(
'Permission Required',
`Please enable health data access in the ${appName}.`,
[
{ text: 'Cancel' },
{ text: 'Open Settings', onPress: () => HealthX.openHealthSettings() }
]
);
}
Handling Missing Stages
function getSleepBreakdown(session: SleepSession) {
// Some stages might not be available on all platforms/devices
const stages = {
light: 0,
deep: 0,
rem: 0,
awake: 0,
};
for (const stage of session.stages) {
switch (stage.type) {
case 'LIGHT':
case 'SLEEPING': // Generic sleep counts as light
stages.light += stage.duration;
break;
case 'DEEP':
stages.deep += stage.duration;
break;
case 'REM':
stages.rem += stage.duration;
break;
case 'AWAKE':
stages.awake += stage.duration;
break;
}
}
return stages;
}