Abstraction around Android Health Connect and iOS HealthKit with unified API
Feel free to contribute ❤️
- Cross-Platform: Works with Android Health Connect and iOS HealthKit
- Generic API: Use
GetHealthDataAsync<TDto>()for type-safe health data retrieval - Unified DTOs: Platform-agnostic data transfer objects with common properties
- Time Range Support: Duration-based metrics implement
IHealthTimeRangeinterface - Write/delete: Possibility to write/delete any health record or activity to/from Android Health/iOS HealthKit
- Aggregate: Platform-native aggregation (sum, average) with cross-source deduplication
- Aggregate by interval: Bucketed aggregation (daily, hourly) using native APIs
- Differential sync: Track changes (upserts/deletions) since a token for efficient data synchronization
- Duplication detection: If you write activity under your app to the ios/android health and at same time you start activity on watch/phone natively. You have possibility to detect these workouts and synchronize it as you need.
| Health Data Type | Android Health Connect | iOS HealthKit | Wrapper Implementation |
|---|---|---|---|
| Steps | ✅ StepsRecord | ✅ StepCount | ✅ StepsDto |
| Weight | ✅ WeightRecord | ✅ BodyMass | ✅ WeightDto |
| Height | ✅ HeightRecord | ✅ Height | ✅ HeightDto |
| Heart Rate | ✅ HeartRateRecord | ✅ HeartRate | ✅ HeartRateDto |
| Active Calories | ✅ ActiveCaloriesBurnedRecord | ✅ ActiveEnergyBurned | ✅ ActiveCaloriesBurnedDto |
| Exercise Session | ✅ ExerciseSessionRecord | ✅ Workout | ✅ WorkoutDto |
| Blood Glucose | ✅ BloodGlucoseRecord | ✅ BloodGlucose | ❌ N/A |
| Body Temperature | ✅ BodyTemperatureRecord | ✅ BodyTemperature | ❌ N/A |
| Oxygen Saturation | ✅ OxygenSaturationRecord | ✅ OxygenSaturation | ❌ N/A |
| Respiratory Rate | ✅ RespiratoryRateRecord | ✅ RespiratoryRate | ❌ N/A |
| Basal Metabolic Rate | ✅ BasalMetabolicRateRecord | ✅ BasalEnergyBurned | ❌ N/A |
| Body Fat | ✅ BodyFatRecord | ✅ BodyFatPercentage | ✅ BodyFatDto |
| Lean Body Mass | ✅ LeanBodyMassRecord | ✅ LeanBodyMass | ❌ N/A |
| Hydration | ✅ HydrationRecord | ✅ DietaryWater | ❌ N/A |
| VO2 Max | ✅ Vo2MaxRecord | ✅ VO2Max | ✅ Vo2MaxDto |
| Resting Heart Rate | ✅ RestingHeartRateRecord | ✅ RestingHeartRate | ❌ N/A |
| Heart Rate Variability | ✅ HeartRateVariabilityRmssdRecord | ✅ HeartRateVariabilitySdnn | ❌ N/A |
| Blood Pressure | ✅ BloodPressureRecord | ✅ Split into Systolic/Diastolic | 🚧 WIP (commented out) |
Register the health service in your MauiProgram.cs:
builder.Services.AddHealth();Then setup all Android and iOS necessities.
- Android (4) docs, docs2
- in Google Play console give Health permissions to the app
- for successful app approval your Policy page must contain
Health data collection and use,Data retention policy - change of
AndroidManifest.xml+ new activity showing privacy policy - add
<queries>element toAndroidManifest.xml(inside<manifest>, outside<application>):This is required on Android 11–13 due to package visibility filtering. Without it,<queries> <package android:name="com.google.android.apps.healthdata" /> </queries>
getSdkStatus()cannot detect Health Connect even when it's installed, causing permission requests to silently fail. On Android 14+ Health Connect is a system service so this isn't strictly needed, but it does no harm. - change of min. Android version to v26
- iOS (3) docs, docs2
- generating new provisioning profile containing HealthKit permissions. These permissions are changed in Identifiers
- adding
Entitlements.plist - adjustment of
Info.plist⚠️ Beware, if your app already exists and targets various devices addingUIRequiredDeviceCapabilitieswithhealthkitcan get your release rejected. For that reason I ommited adding this requirement and I just make sure that I check if the device is capable of usinghealthkit.
After you have everything setup correctly you can use IHealthService from DI container and call it's methods.
If you want an example there is a DemoApp project showing number of steps for Current day
public class HealthExampleService
{
private readonly IHealthService _healthService;
public HealthExampleService(IHealthService healthService)
{
_healthService = healthService;
}
public async Task<List<StepsDto>> GetTodaysStepsAsync()
{
if (!_healthService.IsSupported)
{
return [];
}
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
return await _healthService.GetHealthData<StepsDto>(timeRange);
}
}Duration-based metrics implement IHealthTimeRange:
public async Task AnalyzeStepsData()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var steps = await _healthService.GetHealthData<StepsDto>(timeRange);
foreach (var stepRecord in steps)
{
// Common properties from BaseHealthMetricDto
Console.WriteLine($"ID: {stepRecord.Id}");
Console.WriteLine($"Source: {stepRecord.DataOrigin}");
Console.WriteLine($"Recorded: {stepRecord.Timestamp}");
// Steps-specific data
Console.WriteLine($"Steps: {stepRecord.Count}");
// Time range data (IHealthTimeRange)
Console.WriteLine($"Period: {stepRecord.StartTime} to {stepRecord.EndTime}");
Console.WriteLine($"Duration: {stepRecord.Duration}");
// Type-safe duration checking
if (stepRecord is IHealthTimeRange timeRange)
{
Console.WriteLine($"This measurement lasted {timeRange.Duration.TotalMinutes} minutes");
}
}
}Fetch a specific health record by its platform-specific ID (Android: Health Connect metadata ID, iOS: HealthKit UUID):
public async Task<StepsDto?> GetSpecificRecord(string recordId)
{
return await _healthService.GetHealthRecord<StepsDto>(recordId);
}Note: This API is marked
[Experimental("MH001")]. Suppress the warning with#pragma warning disable MH001.
Delete any health record by its platform-specific ID. You can only delete records created by your application:
public async Task DeleteRecord(string recordId)
{
var isDeleted = await _healthService.DeleteHealthData<StepsDto>(recordId);
if (isDeleted)
{
Console.WriteLine("Record deleted successfully");
}
}Note: This API is marked
[Experimental("MH002")]. Suppress the warning with#pragma warning disable MH002.
Get deduplicated totals or averages using platform-native aggregation. This uses Android's aggregate() API and iOS's HKStatisticsQuery, which properly handle data from multiple health apps (e.g., Samsung Health + Google Fit):
public async Task ShowTodaysSummary()
{
var todayRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
// Cumulative types (steps, calories) return a sum
var steps = await _healthService.GetAggregatedHealthData<StepsDto>(todayRange);
if (steps is not null)
{
Console.WriteLine($"Total steps today: {steps.Value}");
}
// Discrete types (weight, heart rate) return an average
var weight = await _healthService.GetAggregatedHealthData<WeightDto>(todayRange);
if (weight is not null)
{
Console.WriteLine($"Average weight: {weight.Value} {weight.Unit}");
}
}Note: This API is marked
[Experimental("MH003")]. Suppress the warning with#pragma warning disable MH003.
Get aggregated data bucketed by time intervals - ideal for charts and day-by-day views. Uses Android's aggregateGroupByDuration() and iOS's HKStatisticsCollectionQuery:
public async Task ShowWeeklySteps()
{
var weekRange = HealthTimeRange.FromDateTime(
DateTime.Today.AddDays(-6), DateTime.Now);
var dailySteps = await _healthService.GetAggregatedHealthDataByInterval<StepsDto>(
weekRange, TimeSpan.FromDays(1));
foreach (var bucket in dailySteps)
{
Console.WriteLine($"{bucket.StartTime:ddd MMM dd}: {bucket.Value:N0} steps");
}
}Note: This API is marked
[Experimental("MH004")]. Suppress the warning with#pragma warning disable MH004.
Track changes (upserts and deletions) to health data since a given point in time. Uses Android's getChangesToken()/getChanges() and iOS's HKAnchoredObjectQuery. Tokens expire after 30 days.
public class HealthSyncService
{
private readonly IHealthService _healthService;
private string? _syncToken;
// Call once to establish a baseline - captures the current state
public async Task InitializeSync()
{
var dataTypes = new List<HealthDataType>
{
HealthDataType.Steps,
HealthDataType.Weight,
HealthDataType.ActiveCaloriesBurned
};
_syncToken = await _healthService.GetChangesToken(dataTypes);
// Store _syncToken persistently (e.g., Preferences, database)
}
// Call periodically to get new changes since last sync
public async Task SyncChanges()
{
if (_syncToken is null) return;
var result = await _healthService.GetChanges(_syncToken);
if (result is null) return;
foreach (var change in result.Changes)
{
Console.WriteLine($"{change.Type}: {change.RecordId}");
}
// Update token for next call
_syncToken = result.NextToken;
// If more changes available, keep fetching
if (result.HasMore)
{
await SyncChanges();
}
}
}Note: These APIs are marked
[Experimental("MH005")]and[Experimental("MH006")]. Suppress with#pragma warning disable MH005, MH006.
public async Task RequestPermissions()
{
var permissions = new List<HealthPermissionDto>
{
new() { HealthDataType = HealthDataType.Steps, PermissionType = PermissionType.Read },
new() { HealthDataType = HealthDataType.Weight, PermissionType = PermissionType.Read },
new() { HealthDataType = HealthDataType.Height, PermissionType = PermissionType.Read }
};
var result = await _healthService.RequestPermissions(permissions);
if (result.IsSuccess)
{
Console.WriteLine("Permissions granted!");
}
else
{
Console.WriteLine($"Permission error: {result.Error}");
}
}On Android devices with API < 34, Health Connect is a separate app that may need to be installed or updated. The library returns a specific error so you can show custom UI before opening the Play Store:
public async Task RequestPermissionsWithUpdateHandling()
{
var permissions = new List<HealthPermissionDto>
{
new() { HealthDataType = HealthDataType.Steps, PermissionType = PermissionType.Read }
};
var result = await _healthService.RequestPermissions(permissions);
if (result.Error == RequestPermissionError.SdkUnavailableProviderUpdateRequired)
{
// Show your custom UI explaining the update requirement
bool userConfirmed = await DisplayAlert(
"Update Required",
"Health Connect needs to be updated to use health features.",
"Update", "Cancel");
if (userConfirmed)
{
_healthService.OpenStorePageOfHealthProvider(); // Opens Play Store
}
}
}The Activity property on IHealthService provides workout/exercise session management (IHealthWorkoutService) with support for real-time tracking, pause/resume functionality, and duplicate detection.
public async Task<List<WorkoutDto>> GetTodaysWorkouts()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var workouts = await _healthService.Activity.Read(timeRange);
foreach (var workout in workouts)
{
Console.WriteLine($"{workout.ActivityType}: {workout.StartTime:HH:mm} - {workout.EndTime:HH:mm}");
Console.WriteLine($"Duration: {workout.DurationSeconds / 60} minutes");
Console.WriteLine($"Source: {workout.DataOrigin}");
if (workout.EnergyBurned.HasValue)
Console.WriteLine($"Calories: {workout.EnergyBurned:F0} kcal");
if (workout.AverageHeartRate.HasValue)
Console.WriteLine($"Avg HR: {workout.AverageHeartRate:F0} BPM");
}
return workouts;
}public async Task WriteCompletedWorkout()
{
var workout = new WorkoutDto
{
Id = Guid.NewGuid().ToString(),
DataOrigin = "MyApp", -> Your APP data source.
ActivityType = ActivityType.Running,
Title = "Morning Run",
StartTime = DateTimeOffset.Now.AddMinutes(-30),
EndTime = DateTimeOffset.Now,
EnergyBurned = 250,
Distance = 5000 // meters
};
await _healthService.Activity.Write(workout);
}Track workouts in real-time with pause/resume support:
public class WorkoutTracker
{
private readonly IHealthService _healthService;
// Start a new workout session
public async Task StartWorkout()
{
await _healthService.Activity.Start(
ActivityType.Running,
title: "Morning Run",
dataOrigin: "MyApp"
);
}
// Pause the active session
public async Task PauseWorkout()
{
await _healthService.Activity.Pause();
}
// Resume from pause
public async Task ResumeWorkout()
{
await _healthService.Activity.Resume();
}
// End session and save to health store
public async Task<WorkoutDto?> EndWorkout()
{
// Returns the completed workout saved to Health Connect/HealthKit
return await _healthService.Activity.End();
}
// Check session status
public async Task<bool> IsWorkoutRunning() => await _healthService.Activity.IsRunning();
public async Task<bool> IsWorkoutPaused() => await _healthService.Activity.IsPaused();
}When users track workouts from both your app and a smartwatch, duplicates can occur. The FindDuplicates method identifies these by matching:
- Same activity type
- Different data sources (e.g., "MyApp" vs "Apple Watch")
- Start/end times within a configurable threshold
public async Task DetectDuplicateWorkouts()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var workouts = await _healthService.Activity.Read(timeRange);
// Find duplicates with 5-minute threshold
var duplicates = _healthService.Activity.FindDuplicates(
workouts,
appSource: "MyApp", // Your app's DataOrigin
timeThresholdMinutes: 5 // Max time difference to consider as duplicate
);
foreach (var group in duplicates)
{
// Get the workout from your app
var appWorkout = group.AppWorkout;
// Get the workout from watch/other source
var externalWorkout = group.ExternalWorkout;
Console.WriteLine($"Duplicate found:");
Console.WriteLine($" App: {appWorkout?.DataOrigin} at {appWorkout?.StartTime:HH:mm}");
Console.WriteLine($" External: {externalWorkout?.DataOrigin} at {externalWorkout?.StartTime:HH:mm}");
Console.WriteLine($" Time diff: {group.StartTimeDifferenceMinutes:F1} minutes");
// User can decide which to keep - typically keep the watch data
// as it has more accurate heart rate and calorie data
if (appWorkout != null)
{
await _healthService.Activity.Delete(appWorkout);
}
}
}iOS Simulator/Device:
- If no health data exists, open the Health app
- Navigate to the desired metric (e.g., Steps)
- Tap "Add Data" in the top-right corner
- Manually add test data for development
Android Emulator:
- Install Google Health Connect app
- Add sample health data for testing
- Ensure proper permissions are granted
- @aritchie -
https://github.com/shinyorg/Health - @0xc3u -
https://github.com/0xc3u/Plugin.Maui.Health - @EagleDelux -
https://github.com/EagleDelux/androidx.health-connect-demo-.net-maui - @b099l3 -
https://github.com/b099l3/ios-samples/tree/65a4ab1606cfd8beb518731075e4af526c4da4ad/ios8/Fit/Fit
