Plugin.Maui.Health provides the ability to access personal health data in your .NET MAUI application.
Available on NuGet.
Install with the dotnet CLI: dotnet add package Plugin.Maui.Health, or through the NuGet Package Manager in Visual Studio.
| Platform | Minimum Version | Health Backend |
|---|---|---|
| iOS | 15.0+ | Apple HealthKit |
| Android | API 28+ (Android 9+) | Health Connect |
Android note: Health Connect is pre-installed on Android 14+ and available as a standalone app from the Play Store on Android 9–13.
IsSupportedreturnsfalseif Health Connect is not installed/available.
HealthKit is a restricted capability. Before your app can access any health data you must explicitly enable it for your App ID in the Apple Developer Portal, otherwise entitlements signing will fail at build time and HealthKit will be unavailable at runtime.
- Sign in to developer.apple.com and go to Certificates, Identifiers & Profiles → Identifiers.
- Select (or create) the App ID for your app.
- In the Capabilities list, enable HealthKit.
- Save the changes and regenerate any provisioning profiles that use this App ID (Xcode or the portal will prompt you).
HealthKit is available on modern iOS Simulators — you can seed and read data there for development. Always gate calls behind
IsSupported, which returnsfalseon platforms/devices where HealthKit is unavailable.
<PropertyGroup>
<TargetFrameworks>net10.0-ios;net10.0-android</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios'">
<!-- HealthKit features used by this plugin require iOS 15 at minimum -->
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
</PropertyGroup>Create Platforms/iOS/Entitlements.plist in your project. The com.apple.developer.healthkit key is required — without it the app will be rejected during code signing.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
</dict>
</plist>.NET MAUI automatically picks up Platforms/iOS/Entitlements.plist during the iOS build. If you store the file elsewhere, point to it explicitly in your .csproj:
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios'">
<CodesignEntitlements>path\to\Entitlements.plist</CodesignEntitlements>
</PropertyGroup>iOS requires a human-readable explanation for every HealthKit access type. Without these keys the app will crash on the first HealthKit call.
Add the following to Platforms/iOS/Info.plist:
<!-- Required if you read any health data -->
<key>NSHealthShareUsageDescription</key>
<string>This app reads your health data to display your activity and body metrics.</string>
<!-- Required if you write any health data -->
<key>NSHealthUpdateUsageDescription</key>
<string>This app writes health data such as steps and body measurements on your behalf.</string>Use clear, specific descriptions — Apple reviewers reject vague strings.
In MauiProgram.cs, register HealthDataProvider.Default with the DI container:
builder.Services.AddSingleton(HealthDataProvider.Default);Android uses Health Connect as the health data backend. The setup involves .csproj settings, a manifest change, a NuGet conflict fix, and integrating the Health Connect consent flow into your MainActivity.
| Android version | Health Connect |
|---|---|
| Android 14+ (API 34+) | Pre-installed as a system component |
| Android 9–13 (API 28–33) | Must be installed by the user from the Play Store |
| Below Android 9 (API < 28) | Not supported — IsSupported returns false |
Your app should check HealthDataProvider.Default.IsSupported before making any calls and guide users to install Health Connect when it is missing:
if (!healthDataProvider.IsSupported)
{
// Direct the user to install Health Connect from the Play Store.
var intent = new Android.Content.Intent(
Android.Content.Intent.ActionView,
Android.Net.Uri.Parse("market://details?id=com.google.android.apps.healthdata"));
Platform.CurrentActivity?.StartActivity(intent);
return;
}<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
<!-- Health Connect is available from API 28 (Android 9) -->
<AndroidMinSdkVersion>28</AndroidMinSdkVersion>
<!-- Target API 35 for Google Play compliance -->
<AndroidTargetSdkVersion>35</AndroidTargetSdkVersion>
<SupportedOSPlatformVersion>28.0</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<!-- Health Connect and MAUI split the transitive AndroidX Lifecycle graph.
Pin LiveData.Core to the highest required version to resolve the NU1107
conflict at restore time (this is the version NuGet recommends referencing). -->
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData.Core" Version="2.11.0.1" />
</ItemGroup>Edit Platforms/Android/AndroidManifest.xml. Three things are required:
a) <queries> block — tells Android that your app intends to interact with the Health Connect app. Without this the OS will not route Health Connect intents to your app on Android 11+.
b) Privacy-policy / rationale entry point — Health Connect shows a "privacy policy" link on its permission sheet and requires your app to own a screen explaining why it accesses health data. This must be declared two ways, and both are required — on Android 14+ (API 34+) the system will not present the permission request at all if the <activity-alias> is missing, leaving your permission launcher hanging with no result:
- Android 13 and below — an
<activity>with theandroidx.health.ACTION_SHOW_PERMISSIONS_RATIONALEintent filter. - Android 14+ — an
<activity-alias>namedViewPermissionUsageActivity, guarded byandroid.permission.START_VIEW_PERMISSION_USAGE, with theVIEW_PERMISSION_USAGEaction andHEALTH_PERMISSIONScategory, pointing at that same activity.
android:exported="true" is required on Android 12+ for any activity (or alias) with an intent filter. Use a dedicated rationale activity rather than your launcher MainActivity.
c) <uses-permission> declarations — declare every health permission your app needs. Only declare what you actually use; superfluous permissions slow down the consent sheet and may trigger Play Store review.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true">
<!-- b) Android 13 and below: rationale screen shown inside Health Connect.
The activity itself is declared in code via [Activity]/[IntentFilter] —
here we only need the Android 14+ alias that points to it. -->
<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
android:targetActivity="com.companyname.yourapp.PermissionsRationaleActivity"
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>
<!-- a) Allow the OS to discover the Health Connect app -->
<queries>
<package android:name="com.google.android.apps.healthdata" />
</queries>
<!-- c) Health Connect permissions — add only what your app uses -->
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.WRITE_STEPS" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE" />
<!-- ... see the feature matrix below for all available permission strings -->
</manifest>Declare the rationale activity itself in Platforms/Android/. Set an explicit Name so the <activity-alias> above can target it reliably:
[Activity(Name = "com.companyname.yourapp.PermissionsRationaleActivity", Exported = true)]
[IntentFilter(new[] { "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" })]
public class PermissionsRationaleActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
var text = new TextView(this) { Text = "Explain why your app reads/writes health data here." };
text.SetPadding(48, 48, 48, 48);
SetContentView(text);
}
}See the official Health Connect guidance for details.
Health Connect does not auto-launch its permission sheet, but you no longer need to wire up your own
ActivityResultLauncher — the plugin drives the consent flow for you. Just call RequestPermissionAsync
(or RequestWorkoutPermissionAsync); it maps the HealthParameter to the right Health Connect
permissions, launches the consent UI using the current Activity when needed, and reports whether the
permission ended up granted:
// Works on both Android and iOS. On Android the Health Connect consent UI is shown only when the
// permission isn't already granted; on iOS HealthKit presents its own authorization sheet.
var granted = await health.RequestPermissionAsync(
HealthParameter.StepCount, PermissionType.Read | PermissionType.Write);
if (granted)
{
var steps = await health.ReadCountAsync(HealthParameter.StepCount,
DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now);
}Your MainActivity only needs to be a standard MauiAppCompatActivity (the default). No launcher,
no permission-string mapping, no MainActivity.Current plumbing.
Use
CheckPermissionAsyncwhen you only want to know the current grant state without prompting (Android only — on iOS, checking and requesting are the same HealthKit operation).
In MauiProgram.cs, register HealthDataProvider.Default with the DI container:
builder.Services.AddSingleton(HealthDataProvider.Default);Apps that access Health Connect data must comply with Google's Health Connect permissions policy:
- Your app's Privacy Policy must disclose which health data types you access and how they are used.
- In the Play Console, go to App content → Health Connect and complete the declaration form.
- The rationale activity you registered in Step 3 (reachable via both
ACTION_SHOW_PERMISSIONS_RATIONALEand theViewPermissionUsageActivityalias) must display a clear screen explaining why the app needs each permission.
Apps that skip these steps will be rejected during Play Store review.
Register the plugin with the MauiAppBuilder (in MauiProgram.cs):
builder.Services.AddSingleton(HealthDataProvider.Default);You can then depend on IHealth in your ViewModels:
public class HealthViewModel
{
readonly IHealth health;
public HealthViewModel(IHealth health)
{
this.health = health;
}
}Alternatively, use HealthDataProvider.Default directly without DI:
var stepsCount = await HealthDataProvider.Default.ReadCountAsync(
HealthParameter.StepCount,
DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now);| Member | Description |
|---|---|
IsSupported |
Returns true if the platform health backend is available on this device. |
| Method | Description |
|---|---|
RequestPermissionAsync(param, type) |
Requests the specified read/write permission via the platform consent UI and returns whether it ended up granted. The cross-platform way to obtain consent. |
RequestPermissionsAsync(requests) |
Requests several parameters in a single consent prompt. Preferred over multiple RequestPermissionAsync calls — on iOS, presenting several HealthKit sheets in a row can hang the app. |
RequestWorkoutPermissionAsync(type) |
Requests read/write access to workout sessions and GPS routes via the consent UI. |
CheckPermissionAsync(param, type) |
Checks whether the specified read/write permission is currently granted (does not prompt on Android). |
CheckPermissionsAsync(requests) |
Checks whether all of the given permissions are granted, in a single call. The batch companion to CheckPermissionAsync (does not prompt on Android). |
CheckWorkoutPermissionAsync(type) |
Checks whether workout (exercise) read/write permission is currently granted. |
ReadCountAsync(param, from, until) |
Returns the cumulative sum for count-based parameters (e.g. steps, flights climbed). |
ReadLatestAsync(param, from, until, unit) |
Returns the most recent single value within the date range. |
ReadLatestAvailableAsync(param, unit) |
Returns the most recent value ever recorded, as a Sample. |
ReadAverageAsync(param, from, until, unit) |
Returns the average over the date range. |
ReadMinAsync(param, from, until, unit) |
Returns the minimum value over the date range. |
ReadMaxAsync(param, from, until, unit) |
Returns the maximum value over the date range. |
ReadAllAsync(param, from, until, unit) |
Returns all samples in the date range as IEnumerable<Sample>. |
ReadStatisticsAsync(param, from, until, interval, unit) |
Aggregates the parameter into hourly/daily/weekly/monthly buckets (sum for cumulative, average for discrete). |
WriteAsync(param, date, value, unit) |
Writes a single value to the health store. |
WriteAllAsync(param, values, unit) |
Writes several timestamped values in one call — one SaveObjects / InsertRecords (bulk import). |
DeleteAsync(param, from, until) |
Deletes this app's samples for the parameter within the date range. |
UpsertAsync(param, date, value, unit, clientId) |
Writes a value tagged with a stable client id so repeated writes replace the previous one (dedup/upsert). |
ReadWorkoutsAsync(type, from, until) |
Returns all workout sessions in the date range, including GPS routes if available. |
ReadLatestWorkoutAsync(type, from, until) |
Returns the most recent workout session. |
WriteWorkoutAsync(session) |
Writes a workout session (with optional GPS route) to the health store. |
RequestSleepPermissionAsync(type) |
Requests read/write access to sleep data via the consent UI (iOS SleepAnalysis; Android READ_SLEEP / WRITE_SLEEP). |
ReadSleepAsync(from, until) |
Returns the sleep sessions in the date range, each with its per-stage breakdown. |
WriteSleepAsync(session) |
Writes a sleep session, including its stage segments, to the health store. |
RequestMindfulnessPermissionAsync(type) |
Requests read/write access to mindfulness sessions (iOS MindfulSession; Android pending). |
ReadMindfulnessAsync(from, until) / WriteMindfulnessAsync(session) |
Reads/writes mindfulness (meditation) sessions. iOS only for now. |
ReadCharacteristicsAsync() |
Reads the user's static profile (date of birth, biological sex, blood type, skin type, wheelchair use). iOS only (read-only). |
RequestCyclePermissionAsync(type) |
Requests read/write access to menstrual-cycle data (flow, ovulation, sexual activity, intermenstrual bleeding). |
ReadCycleAsync(from, until) / WriteCycleAsync(entry) |
Reads/writes menstrual-cycle entries across all cycle event types. |
All async methods accept an optional CancellationToken as the last parameter.
Sample — returned by read methods:
| Property | Type | Description |
|---|---|---|
From |
DateTimeOffset? |
Sample start time |
Until |
DateTimeOffset? |
Sample end time |
Value |
double? |
The numeric value |
Unit |
string? |
Unit string (e.g. "kg", "count/min") |
Source |
string? |
App/package that recorded the sample (data origin) |
Device |
string? |
Recording device name/model, when reported |
RecordingMethod |
RecordingMethod |
Unknown / Manual / Automatic / ActivelyRecorded |
Description |
string |
Human-readable label |
WorkoutSession — returned by workout read methods:
| Property | Type | Description |
|---|---|---|
WorkoutType |
WorkoutType |
Activity type (Running, Cycling, …) |
From |
DateTimeOffset |
Start time |
Until |
DateTimeOffset |
End time |
TotalDistanceInMeters |
double? |
Total distance |
EnergyBurnedInCalories |
double? |
Active calories burned |
Title |
string? |
Display name (Android only; always null on iOS) |
Route |
IReadOnlyList<RoutePoint> |
GPS track points |
RoutePoint — a single GPS sample within a WorkoutSession.Route:
| Property | Type | Description |
|---|---|---|
Time |
DateTimeOffset |
Timestamp of the point |
Latitude / Longitude |
double |
Coordinates |
AltitudeInMeters |
double? |
Altitude |
HorizontalAccuracyInMeters / VerticalAccuracyInMeters |
double? |
Accuracy |
SleepSession — returned by ReadSleepAsync (and passed to WriteSleepAsync):
| Property | Type | Description |
|---|---|---|
From |
DateTimeOffset |
Start of the night |
Until |
DateTimeOffset |
End of the night |
DurationInSeconds |
double |
Total span (Until − From) |
Source |
string? |
App or device that recorded the session |
Stages |
IReadOnlyList<SleepStageSample> |
Per-stage breakdown of the night |
SleepStageSample — a single stage segment within a SleepSession.Stages:
| Property | Type | Description |
|---|---|---|
From |
DateTimeOffset |
Segment start time |
Until |
DateTimeOffset |
Segment end time |
Stage |
SleepStage |
The stage during this segment |
StatisticsBucket — one time bucket returned by ReadStatisticsAsync:
| Property | Type | Description |
|---|---|---|
From / Until |
DateTimeOffset |
Bucket bounds (local time) |
Value |
double? |
Sum (cumulative params) or average (discrete); null if empty and discrete |
Count |
int |
Number of samples in the bucket |
MindfulnessSession — returned by ReadMindfulnessAsync (and passed to WriteMindfulnessAsync):
| Property | Type | Description |
|---|---|---|
From / Until |
DateTimeOffset |
Session bounds |
DurationInSeconds |
double |
Total span |
Source |
string? |
App/device that recorded it |
CycleEntry — a menstrual-cycle / reproductive-health entry (the relevant value field depends on Type):
| Property | Type | Description |
|---|---|---|
Time |
DateTimeOffset |
When the entry occurred |
Type |
CycleEventType |
Which event this is |
Flow |
MenstruationFlow |
For MenstruationFlow entries |
OvulationResult |
OvulationTestResult |
For OvulationTest entries |
Protection |
SexualActivityProtection |
For SexualActivity entries |
Source |
string? |
App/device that recorded it |
HealthCharacteristics — returned by ReadCharacteristicsAsync (iOS only, read-only):
| Property | Type | Description |
|---|---|---|
DateOfBirth |
DateTimeOffset? |
Date of birth |
BiologicalSex |
BiologicalSex |
Biological sex |
BloodType |
BloodType |
Blood type |
SkinType |
FitzpatrickSkinType |
Fitzpatrick skin type (I–VI) |
WheelchairUse |
WheelchairUse |
Wheelchair use |
| Enum | Values |
|---|---|
PermissionType |
Read, Write — combine with the bitwise OR (PermissionType.Read | PermissionType.Write). |
HealthParameter |
~100 values (steps, heart rate, body metrics, blood values, nutrition/vitamins, mobility …). See the matrix below. |
WorkoutType |
Activity types (Running, Cycling, Swimming, …). Use WorkoutType.Other to match/return all types. |
SleepStage |
Unknown, Awake, InBed, Sleeping, Light, Deep, Rem — normalised across HealthKit and Health Connect. |
StatisticsInterval |
Hourly, Daily, Weekly, Monthly — the bucket size for ReadStatisticsAsync. |
RecordingMethod |
Unknown, Manual, Automatic, ActivelyRecorded — how a Sample was recorded. |
CycleEventType |
MenstruationFlow, OvulationTest, SexualActivity, IntermenstrualBleeding. |
MenstruationFlow |
Unspecified, None, Light, Medium, Heavy. |
OvulationTestResult |
Unspecified, Negative, Positive, High. |
SexualActivityProtection |
Unspecified, Protected, Unprotected. |
BiologicalSex |
NotSet, Female, Male, Other. |
BloodType |
NotSet, APositive, ANegative, BPositive, BNegative, ABPositive, ABNegative, OPositive, ONegative. |
FitzpatrickSkinType |
NotSet, I, II, III, IV, V, VI. |
WheelchairUse |
NotSet, No, Yes. |
Use the Constants.Units static class to avoid typos in unit strings:
Units.Mass.Kilograms // "kg"
Units.Mass.Pounds // "lb"
Units.Length.Meters // "m"
Units.Length.Centimeters // "cm"
Units.Energy.Kilocalories // "kcal"
Units.Concentration.MillilitersPerKilogramPerMinute // for VO2Max
Units.Concentration.MillimolesPerLiter // for BloodGlucoseAlways check IsSupported first, then request the permissions you need. The plugin presents the platform
consent UI (HealthKit sheet on iOS, Health Connect screen on Android) and maps parameters to the right
native permissions — no platform-specific code, launcher, or permission strings required.
if (!health.IsSupported)
return; // Health Connect not installed (Android 11–13) or backend unavailable
// Single parameter
var granted = await health.RequestPermissionAsync(
HealthParameter.StepCount,
PermissionType.Read | PermissionType.Write);Requesting several permissions? Batch them — this shows a single consent prompt and avoids an iOS hang that can occur when multiple HealthKit sheets are presented back-to-back:
var granted = await health.RequestPermissionsAsync(new[]
{
(HealthParameter.StepCount, PermissionType.Read | PermissionType.Write),
(HealthParameter.BodyMass, PermissionType.Read | PermissionType.Write),
(HealthParameter.HeartRate, PermissionType.Read),
(HealthParameter.DietaryVitaminC, PermissionType.Read | PermissionType.Write),
});Check the current grant state without prompting (Android only — on iOS checking and requesting are the same HealthKit operation):
bool granted = await health.CheckPermissionAsync(HealthParameter.HeartRate, PermissionType.Read);Checking several at once? Use the batch companion — it returns true only if every permission is granted:
bool allGranted = await health.CheckPermissionsAsync(new[]
{
(HealthParameter.StepCount, PermissionType.Read | PermissionType.Write),
(HealthParameter.HeartRate, PermissionType.Read),
(HealthParameter.BodyMass, PermissionType.Read | PermissionType.Write),
});var hasPermission = await health.RequestPermissionAsync(HealthParameter.StepCount, PermissionType.Read);
if (hasPermission)
{
var steps = await health.ReadCountAsync(
HealthParameter.StepCount,
DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now);
}var sample = await health.ReadLatestAvailableAsync(HealthParameter.BodyMass, Units.Mass.Kilograms);
double kg = sample?.Value ?? 0;var hasPermission = await health.RequestPermissionAsync(HealthParameter.BodyMass, PermissionType.Write);
if (hasPermission)
{
await health.WriteAsync(HealthParameter.BodyMass, DateTimeOffset.Now, 75.5, Units.Mass.Kilograms);
}var samples = await health.ReadAllAsync(
HealthParameter.HeartRate,
DateTimeOffset.Now.AddHours(-6),
DateTimeOffset.Now,
Units.Others.CountPerMinute);var workouts = await health.ReadWorkoutsAsync(
WorkoutType.Running,
DateTimeOffset.Now.AddDays(-7),
DateTimeOffset.Now);
foreach (var w in workouts)
{
Console.WriteLine($"{w.WorkoutType}: {w.TotalDistanceInMeters:F0} m, {w.Route.Count} GPS points");
}var from = DateTimeOffset.Now.AddDays(-7);
var until = DateTimeOffset.Now;
double? avgHr = await health.ReadAverageAsync(HealthParameter.HeartRate, from, until, Units.Others.CountPerMinute);
double? minHr = await health.ReadMinAsync(HealthParameter.HeartRate, from, until, Units.Others.CountPerMinute);
double? maxHr = await health.ReadMaxAsync(HealthParameter.HeartRate, from, until, Units.Others.CountPerMinute);// Latest reading inside a window (null if none in range)
double? latestGlucose = await health.ReadLatestAsync(
HealthParameter.BloodGlucose, from, until, Units.Concentration.MillimolesPerLiter);
// Latest reading ever recorded, with full sample metadata
Sample? sample = await health.ReadLatestAvailableAsync(HealthParameter.BodyMass, Units.Mass.Kilograms);var session = new WorkoutSession
{
WorkoutType = WorkoutType.Running,
From = DateTimeOffset.Now.AddMinutes(-30),
Until = DateTimeOffset.Now,
TotalDistanceInMeters = 5200,
EnergyBurnedInCalories = 410,
Route = new List<RoutePoint>
{
new() { Time = DateTimeOffset.Now.AddMinutes(-30), Latitude = 47.37, Longitude = 8.54, AltitudeInMeters = 408 },
new() { Time = DateTimeOffset.Now, Latitude = 47.38, Longitude = 8.55, AltitudeInMeters = 412 },
}
};
if (await health.RequestWorkoutPermissionAsync(PermissionType.Write))
await health.WriteWorkoutAsync(session);Sleep is session data (not a single quantity): each SleepSession carries a list of SleepStageSample
segments describing the night's hypnogram. Request the dedicated sleep permission, then read or write.
if (await health.RequestSleepPermissionAsync(PermissionType.Read | PermissionType.Write))
{
// Read the last two nights, with per-stage breakdown
var sessions = await health.ReadSleepAsync(DateTimeOffset.Now.AddDays(-2), DateTimeOffset.Now);
foreach (var night in sessions)
{
double asleepMinutes = night.Stages
.Where(s => s.Stage is SleepStage.Light or SleepStage.Deep or SleepStage.Rem or SleepStage.Sleeping)
.Sum(s => (s.Until - s.From).TotalMinutes);
Console.WriteLine($"{night.From:ddd}: {asleepMinutes:F0} min asleep, {night.Stages.Count} segments");
}
}// Write a night's sleep with its stages
var wake = DateTimeOffset.Now.Date.AddHours(7);
var session = new SleepSession
{
From = wake.AddHours(-8),
Until = wake,
DurationInSeconds = TimeSpan.FromHours(8).TotalSeconds,
Stages = new List<SleepStageSample>
{
new() { From = wake.AddHours(-8), Until = wake.AddHours(-7), Stage = SleepStage.Light },
new() { From = wake.AddHours(-7), Until = wake.AddHours(-6), Stage = SleepStage.Deep },
new() { From = wake.AddHours(-6), Until = wake.AddHours(-5), Stage = SleepStage.Rem },
new() { From = wake.AddHours(-5), Until = wake, Stage = SleepStage.Light },
}
};
await health.WriteSleepAsync(session);// Daily step totals for the last week (cumulative params are summed, discrete ones averaged).
var days = await health.ReadStatisticsAsync(
HealthParameter.StepCount,
DateTimeOffset.Now.Date.AddDays(-6),
DateTimeOffset.Now,
StatisticsInterval.Daily,
Units.Others.Count);
foreach (var bucket in days)
Console.WriteLine($"{bucket.From:ddd}: {bucket.Value:0} steps");// Write many samples in one call (bulk import).
await health.WriteAllAsync(HealthParameter.BodyMass, new[]
{
(DateTimeOffset.Now.AddDays(-2), 74.8),
(DateTimeOffset.Now.AddDays(-1), 74.5),
(DateTimeOffset.Now, 74.6),
}, Units.Mass.Kilograms);
// Delete this app's samples for a parameter in a range.
await health.DeleteAsync(HealthParameter.BodyMass, DateTimeOffset.Now.AddDays(-7), DateTimeOffset.Now);
// Upsert: repeated writes with the same client id replace the previous value (no duplicates).
await health.UpsertAsync(HealthParameter.BodyMass, DateTimeOffset.Now, 75.1, Units.Mass.Kilograms, clientId: "morning-weigh-in");var samples = await health.ReadAllAsync(HealthParameter.HeartRate, from, until, Units.Others.CountPerMinute);
foreach (var s in samples)
Console.WriteLine($"{s.Value} bpm — {s.Source} / {s.Device ?? "?"} ({s.RecordingMethod})");if (await health.RequestMindfulnessPermissionAsync(PermissionType.Read | PermissionType.Write))
{
await health.WriteMindfulnessAsync(new MindfulnessSession
{
From = DateTimeOffset.Now.AddMinutes(-10),
Until = DateTimeOffset.Now,
});
var sessions = await health.ReadMindfulnessAsync(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now);
}if (await health.RequestCyclePermissionAsync(PermissionType.Read | PermissionType.Write))
{
await health.WriteCycleAsync(new CycleEntry
{
Time = DateTimeOffset.Now,
Type = CycleEventType.MenstruationFlow,
Flow = MenstruationFlow.Medium,
});
var entries = await health.ReadCycleAsync(DateTimeOffset.Now.AddDays(-30), DateTimeOffset.Now);
foreach (var e in entries.Where(e => e.Type == CycleEventType.OvulationTest))
Console.WriteLine($"{e.Time:d}: ovulation {e.OvulationResult}");
}var profile = await health.ReadCharacteristicsAsync();
Console.WriteLine($"{profile.BiologicalSex}, born {profile.DateOfBirth:d}, blood {profile.BloodType}");Every method throws a single exception type — HealthException — for hard failures (unsupported
parameter, backend unavailable, native error). It carries context and preserves the original platform
error as InnerException. Reads simply return null/0/an empty list when there is no data (that is not
an error).
using Plugin.Maui.Health.Exceptions;
try
{
var steps = await health.ReadCountAsync(HealthParameter.StepCount, from, until);
}
catch (HealthException ex)
{
// ex.Message is human-readable, e.g.
// "Plugin.Maui.Health: ReadCountAsync failed on Android: <native detail>"
Console.WriteLine($"{ex.Operation} on {ex.Platform} [{ex.Parameter}]: {ex.Message}");
Console.WriteLine(ex.InnerException); // original HealthKit / Health Connect error
}Legend: Y = supported, — = not supported on this platform
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
BodyMassIndex |
Y | — | — |
BodyMass |
Y | Y | health.READ_WEIGHT / WRITE_WEIGHT |
Height |
Y | Y | health.READ_HEIGHT / WRITE_HEIGHT |
BodyFatPercentage |
Y | Y | health.READ_BODY_FAT / WRITE_BODY_FAT |
LeanBodyMass |
Y | Y | health.READ_LEAN_BODY_MASS / WRITE_LEAN_BODY_MASS |
WaistCircumference |
Y | — | — |
BoneMass |
— | Y | health.READ_BONE_MASS / WRITE_BONE_MASS |
BodyWaterMass |
— | Y | health.READ_BODY_WATER_MASS / WRITE_BODY_WATER_MASS |
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
HeartRate |
Y | Y | health.READ_HEART_RATE / WRITE_HEART_RATE |
RestingHeartRate |
Y | Y | health.READ_RESTING_HEART_RATE / WRITE_RESTING_HEART_RATE |
WalkingHeartRateAverage |
Y | — | — |
HeartRateVariabilitySdnn |
Y | Y | health.READ_HEART_RATE_VARIABILITY / WRITE_HEART_RATE_VARIABILITY |
HeartRateRecoveryOneMinute |
Y | — | — |
AtrialFibrillationBurden |
Y | — | — |
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
StepCount |
Y | Y | health.READ_STEPS / WRITE_STEPS |
DistanceWalkingRunning |
Y | Y | health.READ_DISTANCE / WRITE_DISTANCE |
DistanceCycling |
Y | Y | health.READ_DISTANCE / WRITE_DISTANCE |
DistanceSwimming |
Y | — | — |
DistanceWheelchair |
Y | — | — |
DistanceDownhillSnowSports |
Y | — | — |
ActiveEnergyBurned |
Y | Y | health.READ_ACTIVE_CALORIES_BURNED / WRITE_ACTIVE_CALORIES_BURNED |
BasalEnergyBurned |
Y | Y | health.READ_BASAL_METABOLIC_RATE / WRITE_BASAL_METABOLIC_RATE |
TotalEnergyBurned |
— | Y | health.READ_TOTAL_CALORIES_BURNED / WRITE_TOTAL_CALORIES_BURNED |
FlightsClimbed |
Y | Y | health.READ_FLOORS_CLIMBED / WRITE_FLOORS_CLIMBED |
ElevationGained |
— | Y | health.READ_ELEVATION_GAINED / WRITE_ELEVATION_GAINED |
ExerciseTime |
Y | Y | health.READ_EXERCISE / WRITE_EXERCISE |
StandTime |
Y | — | — |
MoveTime |
Y | — | — |
PushCount |
Y | Y | health.READ_WHEELCHAIR_PUSHES / WRITE_WHEELCHAIR_PUSHES |
SwimmingStrokeCount |
Y | — | — |
NikeFuel |
Y | — | — |
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
BloodGlucose |
Y | Y | health.READ_BLOOD_GLUCOSE / WRITE_BLOOD_GLUCOSE |
BloodPressureSystolic |
Y | Y | health.READ_BLOOD_PRESSURE / WRITE_BLOOD_PRESSURE |
BloodPressureDiastolic |
Y | Y | health.READ_BLOOD_PRESSURE / WRITE_BLOOD_PRESSURE |
OxygenSaturation |
Y | Y | health.READ_OXYGEN_SATURATION / WRITE_OXYGEN_SATURATION |
BodyTemperature |
Y | Y | health.READ_BODY_TEMPERATURE / WRITE_BODY_TEMPERATURE |
BasalBodyTemperature |
Y | Y | health.READ_BASAL_BODY_TEMPERATURE / WRITE_BASAL_BODY_TEMPERATURE |
RespiratoryRate |
Y | Y | health.READ_RESPIRATORY_RATE / WRITE_RESPIRATORY_RATE |
VO2Max |
Y | Y | health.READ_VO2_MAX / WRITE_VO2_MAX |
InsulinDelivery |
Y | — | — |
BloodAlcoholContent |
Y | — | — |
PeripheralPerfusionIndex |
Y | — | — |
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
ForcedVitalCapacity |
Y | — | — |
ForcedExpiratoryVolume1 |
Y | — | — |
PeakExpiratoryFlowRate |
Y | — | — |
InhalerUsage |
Y | — | — |
| HealthParameter | iOS | Android | Android Permission |
|---|---|---|---|
WalkingSpeed |
Y | Y | health.READ_SPEED / WRITE_SPEED |
RunningSpeed |
Y | Y | health.READ_SPEED / WRITE_SPEED |
RunningPower |
Y | Y | health.READ_POWER / WRITE_POWER |
WalkingStepLength |
Y | — | — |
WalkingAsymmetryPercentage |
Y | — | — |
WalkingDoubleSupportPercentage |
Y | — | — |
WalkingSteadiness |
Y | — | — |
SixMinuteWalkTestDistance |
Y | — | — |
StairAscentSpeed |
Y | — | — |
StairDescentSpeed |
Y | — | — |
RunningGroundContactTime |
Y | — | — |
RunningStrideLength |
Y | — | — |
RunningVerticalOscillation |
Y | — | — |
| Feature | iOS | Android | Android Permission |
|---|---|---|---|
| Read workouts | Y | Y | health.READ_EXERCISE |
| Write workouts | Y | Y | health.WRITE_EXERCISE |
| GPS route (read) | Y | Y | health.READ_EXERCISE_ROUTES |
| GPS route (write) | Y | Y | health.WRITE_EXERCISE_ROUTE |
| Feature | iOS | Android | Android Permission |
|---|---|---|---|
| Read sleep sessions | Y | Y | health.READ_SLEEP |
| Write sleep sessions | Y | Y | health.WRITE_SLEEP |
| Per-stage breakdown (Light / Deep / REM / Awake) | Y | Y | — |
On iOS, sleep is stored as HKCategoryType.SleepAnalysis samples, which the plugin groups into sessions by time gaps. On Android, each SleepSessionRecord (with its Stage list) maps to one SleepSession.
| Feature | iOS | Android | Android Permission |
|---|---|---|---|
| Read / write mindfulness sessions | Y | — | health.READ_MINDFULNESS / WRITE_MINDFULNESS |
iOS maps to HKCategoryType.MindfulSession. Android support is pending a Health Connect binding that exposes MindfulnessSessionRecord (calls throw HealthException until then).
CycleEventType |
iOS | Android | Android Permission |
|---|---|---|---|
MenstruationFlow |
Y | Y | health.READ_MENSTRUATION / WRITE_MENSTRUATION |
OvulationTest |
Y | Y | health.READ_OVULATION_TEST / WRITE_OVULATION_TEST |
SexualActivity |
Y | Y | health.READ_SEXUAL_ACTIVITY / WRITE_SEXUAL_ACTIVITY |
IntermenstrualBleeding |
Y | Y | health.READ_INTERMENSTRUAL_BLEEDING / WRITE_INTERMENSTRUAL_BLEEDING |
Use RequestCyclePermissionAsync / ReadCycleAsync / WriteCycleAsync with a CycleEntry. On iOS, sexual-activity protection is write-only (reads back as Unspecified).
| Feature | iOS | Android |
|---|---|---|
ReadCharacteristicsAsync (date of birth, biological sex, blood type, skin type, wheelchair use) |
Y | — |
Android Health Connect has no characteristic types, so this throws HealthException there.
All nutrition parameters use health.READ_NUTRITION / WRITE_NUTRITION on Android. Water uses health.READ_HYDRATION / WRITE_HYDRATION.
| HealthParameter | iOS | Android |
|---|---|---|
DietaryWater |
Y | Y |
DietaryEnergyConsumed |
Y | Y |
DietaryProtein |
Y | Y |
DietaryCarbohydrates |
Y | Y |
DietaryFatTotal |
Y | Y |
DietaryFatSaturated |
Y | Y |
DietaryFatMonounsaturated |
Y | Y |
DietaryFatPolyunsaturated |
Y | Y |
DietaryFiber |
Y | Y |
DietarySugar |
Y | Y |
DietaryCholesterol |
Y | Y |
DietarySodium |
Y | Y |
DietaryCalcium |
Y | Y |
DietaryIron |
Y | Y |
DietaryPotassium |
Y | Y |
DietaryVitaminA |
Y | Y |
DietaryVitaminB6 |
Y | Y |
DietaryVitaminB12 |
Y | Y |
DietaryVitaminC |
Y | Y |
DietaryVitaminD |
Y | Y |
DietaryVitaminE |
Y | Y |
DietaryVitaminK |
Y | Y |
DietaryBiotin |
Y | Y |
DietaryCaffeine |
Y | Y |
DietaryChloride |
Y | Y |
DietaryChromium |
Y | Y |
DietaryCopper |
Y | Y |
DietaryFolate |
Y | Y |
DietaryIodine |
Y | Y |
DietaryMagnesium |
Y | Y |
DietaryManganese |
Y | Y |
DietaryMolybdenum |
Y | Y |
DietaryNiacin |
Y | Y |
DietaryPantothenicAcid |
Y | Y |
DietaryPhosphorus |
Y | Y |
DietaryRiboflavin |
Y | Y |
DietarySelenium |
Y | Y |
DietaryThiamin |
Y | Y |
DietaryZinc |
Y | Y |
| HealthParameter | iOS | Android |
|---|---|---|
UVExposure |
Y | — |
ElectrodermalActivity |
Y | — |
EnvironmentalAudioExposure |
Y | — |
HeadphoneAudioExposure |
Y | — |
NumberOfTimesFallen |
Y | — |
NumberOfAlcoholicBeverages |
Y | — |
SleepingWristTemperature |
Y | — |
UnderwaterDepth |
Y | — |
WaterTemperature |
Y | — |
The sample app is a card-based dashboard with charts drawn using MAUI.Graphics — a goal ring, a weekly steps bar chart, a weight-trend line chart, a heart-rate time series, a workout GPS-route map and a sleep-stage timeline. On first launch it seeds representative data into the device's health store via the plugin and reads it back, so every screen reflects genuine round-tripped data on both Android (Health Connect) and iOS (HealthKit):
Dashboard — a "Today" activity hero (goal ring + weekly steps) plus an "at a glance" grid of live tiles (heart rate, sleep, weight, workouts, vitamins, permissions), each tapping through to its detail screen:
| Android (Health Connect) | iOS (HealthKit) |
|---|---|
![]() |
![]() |
Detail screens — BMI ring + weight-trend line chart, a heart-rate time-series line chart, and a vitamin-intake bar chart:
| Body Measurements | Heart Rate | Vitamins |
|---|---|---|
![]() |
![]() |
![]() |
Workouts & Sleep — a workout with its GPS route drawn from real route points, and a sleep-stage timeline with per-stage totals. Both round-trip through the plugin on Android (Health Connect) and iOS (HealthKit):
| Workouts (GPS route) | Sleep (stage timeline) |
|---|---|
![]() |
![]() |
Try the sample app to test the plugin on your own device.
- Always pass the correct
HealthParametertogether with its matching unit string. Use theConstants.Unitshelper class to avoid typos. - On Android,
ReadAllAsyncreturns at most 1,000 records per call. For high-frequency sensors (heart rate, speed) over long time ranges, split your query into smaller intervals. - On iOS,
CheckPermissionAsyncfor read permission always returnstrueafter the user has seen the consent sheet — HealthKit does not reveal whether read access was actually granted. Only write permission denials are detectable. - On Android,
CheckPermissionAsyncreturnsfalsefor parameters that are not supported by Health Connect (see matrix above). These parameters will also throwHealthExceptionif passed to any read/write method.
This project could not have came to be without these projects and people, thank you! <3
- We thank Gerald Versluis (@jfversluis) for his excellent template Plugin.Maui.Template






