Skip to content

0xc3u/Plugin.Maui.Health

Repository files navigation

Plugin.Maui.Health

Plugin.Maui.Health

Plugin.Maui.Health provides the ability to access personal health data in your .NET MAUI application.

Build Status

ci

Install Plugin

NuGet

Available on NuGet.

Install with the dotnet CLI: dotnet add package Plugin.Maui.Health, or through the NuGet Package Manager in Visual Studio.

Platform Support

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. IsSupported returns false if Health Connect is not installed/available.

Setup Guide

iOS Setup

Step 1 — Apple Developer Portal

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.

  1. Sign in to developer.apple.com and go to Certificates, Identifiers & Profiles → Identifiers.
  2. Select (or create) the App ID for your app.
  3. In the Capabilities list, enable HealthKit.
  4. 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 returns false on platforms/devices where HealthKit is unavailable.


Step 2 — .csproj: target framework and minimum OS version

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

Step 3 — Entitlements.plist

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>

Step 4 — Info.plist: usage descriptions

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.


Step 5 — Register the plugin

In MauiProgram.cs, register HealthDataProvider.Default with the DI container:

builder.Services.AddSingleton(HealthDataProvider.Default);

Android Setup

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.


Step 1 — Health Connect availability on device

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

Step 2 — .csproj: SDK versions and NuGet conflict fix

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

Step 3 — AndroidManifest.xml

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 the androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE intent filter.
  • Android 14+ — an <activity-alias> named ViewPermissionUsageActivity, guarded by android.permission.START_VIEW_PERMISSION_USAGE, with the VIEW_PERMISSION_USAGE action and HEALTH_PERMISSIONS category, 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.


Step 4 — Requesting consent

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 CheckPermissionAsync when you only want to know the current grant state without prompting (Android only — on iOS, checking and requesting are the same HealthKit operation).


Step 5 — Register the plugin

In MauiProgram.cs, register HealthDataProvider.Default with the DI container:

builder.Services.AddSingleton(HealthDataProvider.Default);

Step 6 — Google Play Store requirements

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_RATIONALE and the ViewPermissionUsageActivity alias) must display a clear screen explaining why the app needs each permission.

Apps that skip these steps will be rejected during Play Store review.


Dependency Injection

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

Static Usage

Alternatively, use HealthDataProvider.Default directly without DI:

var stepsCount = await HealthDataProvider.Default.ReadCountAsync(
    HealthParameter.StepCount,
    DateTimeOffset.Now.AddDays(-1),
    DateTimeOffset.Now);

API Reference

Properties

Member Description
IsSupported Returns true if the platform health backend is available on this device.

Methods

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.

Models

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

Enums

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.

Units

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 BloodGlucose

Code Examples

Request permissions

Always 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),
});

Read step count (last 24 hours)

var hasPermission = await health.RequestPermissionAsync(HealthParameter.StepCount, PermissionType.Read);
if (hasPermission)
{
    var steps = await health.ReadCountAsync(
        HealthParameter.StepCount,
        DateTimeOffset.Now.AddDays(-1),
        DateTimeOffset.Now);
}

Read latest body mass

var sample = await health.ReadLatestAvailableAsync(HealthParameter.BodyMass, Units.Mass.Kilograms);
double kg = sample?.Value ?? 0;

Write body mass

var hasPermission = await health.RequestPermissionAsync(HealthParameter.BodyMass, PermissionType.Write);
if (hasPermission)
{
    await health.WriteAsync(HealthParameter.BodyMass, DateTimeOffset.Now, 75.5, Units.Mass.Kilograms);
}

Read all heart rate samples

var samples = await health.ReadAllAsync(
    HealthParameter.HeartRate,
    DateTimeOffset.Now.AddHours(-6),
    DateTimeOffset.Now,
    Units.Others.CountPerMinute);

Read workouts with GPS route

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");
}

Statistics over a range (average / min / max)

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

Most recent value within a range

// 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);

Write a workout (with an optional GPS route)

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

Read & write sleep sessions

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

Bucketed statistics (daily / weekly …)

// 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");

Batch write, delete, and upsert

// 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");

Provenance on samples

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})");

Mindfulness sessions (iOS)

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);
}

Reproductive / cycle tracking

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}");
}

Profile characteristics (iOS)

var profile = await health.ReadCharacteristicsAsync();
Console.WriteLine($"{profile.BiologicalSex}, born {profile.DateOfBirth:d}, blood {profile.BloodType}");

Error handling

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
}

Feature & Permission Matrix

Legend: Y = supported, = not supported on this platform

Body Measurements

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

Heart & Cardiovascular

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

Activity & Fitness

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

Vitals & Clinical

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

Respiratory

HealthParameter iOS Android Android Permission
ForcedVitalCapacity Y
ForcedExpiratoryVolume1 Y
PeakExpiratoryFlowRate Y
InhalerUsage Y

Mobility & Gait

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

Workouts

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

Sleep

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.

Mindfulness

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

Reproductive / Cycle tracking

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

Characteristics (iOS only, read-only)

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.

Nutrition & Hydration

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

Environmental & Other (iOS only)

HealthParameter iOS Android
UVExposure Y
ElectrodermalActivity Y
EnvironmentalAudioExposure Y
HeadphoneAudioExposure Y
NumberOfTimesFallen Y
NumberOfAlcoholicBeverages Y
SleepingWristTemperature Y
UnderwaterDepth Y
WaterTemperature Y

Sample App

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)
Sample app dashboard on Android Sample app dashboard on iOS

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
Body Measurements screen Heart Rate screen Vitamins screen

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)
Workouts screen with GPS route Sleep screen with stage timeline

Try the sample app to test the plugin on your own device.


Remarks

  • Always pass the correct HealthParameter together with its matching unit string. Use the Constants.Units helper class to avoid typos.
  • On Android, ReadAllAsync returns 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, CheckPermissionAsync for read permission always returns true after 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, CheckPermissionAsync returns false for parameters that are not supported by Health Connect (see matrix above). These parameters will also throw HealthException if passed to any read/write method.

Acknowledgements

This project could not have came to be without these projects and people, thank you! <3

About

Cross-platform .NET MAUI plugin to read & write health and fitness data via Apple HealthKit (iOS) and Android Health Connect.

Topics

Resources

License

Stars

14 stars

Watchers

2 watching

Forks

Packages

 
 
 

Contributors

Languages