name: shiny-health description: Generate cross-platform health data queries, write health data, and observe real-time health changes using Shiny Health for Apple HealthKit and Android Health Connect auto_invoke: true triggers:
- "health data"
- "health kit"
- "healthkit"
- "health connect"
- "step count"
- "heart rate"
- "calories"
- "distance"
- "weight"
- "height"
- "body fat"
- "blood pressure"
- "oxygen saturation"
- "sleep duration"
- "hydration"
- "resting heart rate"
- "health metrics"
- "health permissions"
- "IHealthService"
- "DataType"
- "NumericHealthResult"
- "BloodPressureResult"
- "HealthResult"
- "GetStepCounts"
- "GetCalories"
- "GetDistances"
- "GetAverageHeartRate"
- "GetWeight"
- "GetHeight"
- "GetBodyFatPercentage"
- "GetRestingHeartRate"
- "GetBloodPressure"
- "GetOxygenSaturation"
- "GetSleepDuration"
- "GetHydration"
- "RequestPermissions"
- "PermissionType"
- "write health"
- "write steps"
- "write weight"
- "write calories"
- "log health"
- "save health"
- "record health"
- "Observe"
- "observe health"
- "monitor health"
- "watch health"
- "real-time health"
- "health changes"
- "AddHealthIntegration"
- "Shiny.Health"
Shiny Health Skill
Triggers
- health data
- health kit
- healthkit
- health connect
- step count
- heart rate
- calories
- distance
- weight
- height
- body fat
- blood pressure
- oxygen saturation
- sleep duration
- hydration
- resting heart rate
- health metrics
- health permissions
- IHealthService
- DataType
- NumericHealthResult
- BloodPressureResult
- HealthResult
- GetStepCounts
- GetCalories
- GetDistances
- GetAverageHeartRate
- GetWeight
- GetHeight
- GetBodyFatPercentage
- GetRestingHeartRate
- GetBloodPressure
- GetOxygenSaturation
- GetSleepDuration
- GetHydration
- RequestPermissions
- PermissionType
- write health
- write steps
- write weight
- write calories
- log health
- save health
- record health
- Observe
- observe health
- monitor health
- watch health
- real-time health
- health changes
- AddHealthIntegration
- Shiny.Health
You are an expert in Shiny Health, a .NET MAUI library that provides a unified API for reading and writing health data from Apple HealthKit (iOS) and Android Health Connect.
When to Use This Skill
Invoke this skill when the user wants to:
- Query health metrics (steps, heart rate, calories, distance, weight, height, body fat, blood pressure, oxygen saturation, sleep, hydration)
- Write/log health data (steps, weight, hydration, blood pressure, etc.)
- Observe real-time health data changes (e.g., monitor step counts or heart rate as they are recorded)
- Set up health data access in a .NET MAUI application
- Request health data permissions (read and/or write) on iOS or Android
- Work with time-bucketed health data aggregations
- Understand which health metrics are available cross-platform
- Configure iOS HealthKit entitlements or Android Health Connect permissions
Library Overview
GitHub: https://github.com/shinyorg/health
NuGet: Shiny.Health
Namespace: Shiny.Health
Shiny Health provides:
- A single
IHealthServiceinterface that works on both iOS and Android - Read and write support for all 12 cross-platform health metrics
- Real-time observation of health data changes via
IAsyncEnumerable<HealthResult> - Time-bucketed aggregate queries at minute, hour, or day intervals
- Permission management with read/write granularity via
PermissionType - AOT-compatible implementation (no .NET reflection)
Setup
1. Install NuGet Package
dotnet add package Shiny.Health
2. Configure in MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp
.CreateBuilder()
.UseMauiApp<App>()
.UseShiny();
builder.Services.AddHealthIntegration();
return builder.Build();
}
3. iOS Setup
Your app requires a provisioning profile with HealthKit capabilities enabled.
Info.plist:
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>healthkit</string>
</array>
<key>NSHealthUpdateUsageDescription</key>
<string>We need access to update your health data</string>
<key>NSHealthShareUsageDescription</key>
<string>We need access to read your health data</string>
Entitlements.plist:
<key>com.apple.developer.healthkit</key>
<true />
<key>com.apple.developer.healthkit.background-delivery</key>
<true />
4. Android Setup (Health Connect)
Android uses Health Connect (the replacement for the deprecated Google Fit API). Health Connect requires Android 9 (API 28) or higher.
AndroidManifest.xml:
<!-- Declare which health data your app reads -->
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_TOTAL_ENERGY_BURNED" />
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
<uses-permission android:name="android.permission.health.READ_WEIGHT" />
<uses-permission android:name="android.permission.health.READ_HEIGHT" />
<uses-permission android:name="android.permission.health.READ_BODY_FAT" />
<uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
<uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION" />
<uses-permission android:name="android.permission.health.READ_SLEEP" />
<uses-permission android:name="android.permission.health.READ_HYDRATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<!-- Optional: declare which health data your app writes (only include the types you need) -->
<uses-permission android:name="android.permission.health.WRITE_STEPS" />
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE" />
<uses-permission android:name="android.permission.health.WRITE_TOTAL_ENERGY_BURNED" />
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
<uses-permission android:name="android.permission.health.WRITE_WEIGHT" />
<uses-permission android:name="android.permission.health.WRITE_HEIGHT" />
<uses-permission android:name="android.permission.health.WRITE_BODY_FAT" />
<uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE" />
<uses-permission android:name="android.permission.health.WRITE_BLOOD_PRESSURE" />
<uses-permission android:name="android.permission.health.WRITE_OXYGEN_SATURATION" />
<uses-permission android:name="android.permission.health.WRITE_SLEEP" />
<uses-permission android:name="android.permission.health.WRITE_HYDRATION" />
<!-- Allow your app to discover Health Connect -->
<queries>
<package android:name="com.google.android.apps.healthdata" />
</queries>
Requirements:
- The Health Connect app must be installed on the device
- Minimum SDK version must be set to 28 (Android 9)
API Reference
Core Types
// Permission type for read/write access
[Flags]
public enum PermissionType
{
Read = 1,
Write = 2,
ReadWrite = Read | Write
}
// Time interval for bucketed queries
public enum Interval { Minutes, Hours, Days }
// Available health data types
public enum DataType
{
StepCount, HeartRate, Calories, Distance,
Weight, Height, BodyFatPercentage, RestingHeartRate,
BloodPressure, OxygenSaturation, SleepDuration, Hydration
}
// Result for single-value metrics
public record NumericHealthResult(
DataType DataType,
DateTimeOffset Start,
DateTimeOffset End,
double Value
) : HealthResult(DataType, Start, End);
// Result for blood pressure (dual-value)
public record BloodPressureResult(
DateTimeOffset Start,
DateTimeOffset End,
double Systolic,
double Diastolic
) : HealthResult(DataType.BloodPressure, Start, End);
IHealthService Interface
public interface IHealthService
{
// Observe real-time health data changes (forward-only, yields new samples as recorded)
// iOS: push-based HKAnchoredObjectQuery; Android: polls Health Connect change tokens
IAsyncEnumerable<HealthResult> Observe(DataType dataType, TimeSpan? pollingInterval = null, CancellationToken cancelToken = default);
// Request read permissions (backward compatible)
Task<IEnumerable<(DataType Type, bool Success)>> RequestPermissions(params DataType[] dataTypes);
// Request read, write, or both permissions (uniform for all types)
Task<IEnumerable<(DataType Type, bool Success)>> RequestPermissions(PermissionType permissionType, params DataType[] dataTypes);
// Request per-metric read/write permissions in a single call
Task<IEnumerable<(DataType Type, bool Success)>> RequestPermissions(params (PermissionType Permission, DataType Type)[] permissions);
// Write health data
Task Write(NumericHealthResult result, CancellationToken cancelToken = default);
Task Write(BloodPressureResult result, CancellationToken cancelToken = default);
// Activity metrics
Task<IList<NumericHealthResult>> GetStepCounts(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetAverageHeartRate(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetCalories(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetDistances(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
// Body metrics
Task<IList<NumericHealthResult>> GetWeight(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetHeight(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetBodyFatPercentage(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetRestingHeartRate(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
// Vitals
Task<IList<BloodPressureResult>> GetBloodPressure(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetOxygenSaturation(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
// Lifestyle
Task<IList<NumericHealthResult>> GetSleepDuration(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
Task<IList<NumericHealthResult>> GetHydration(DateTimeOffset start, DateTimeOffset end, Interval interval, CancellationToken cancelToken = default);
}
Supported Metrics
| Metric | Unit | iOS (HealthKit) | Android (Health Connect) |
|---|---|---|---|
| Step Count | count | StepCount | StepsRecord |
| Heart Rate | bpm | HeartRate | HeartRateRecord |
| Calories | kcal | ActiveEnergyBurned | TotalCaloriesBurnedRecord |
| Distance | meters | DistanceWalkingRunning | DistanceRecord |
| Weight | kg | BodyMass | WeightRecord |
| Height | meters | Height | HeightRecord |
| Body Fat % | % | BodyFatPercentage | BodyFatRecord |
| Resting Heart Rate | bpm | RestingHeartRate | RestingHeartRateRecord |
| Blood Pressure | mmHg | BloodPressureSystolic/Diastolic | BloodPressureRecord |
| Oxygen Saturation | % | OxygenSaturation | OxygenSaturationRecord |
| Sleep Duration | hours | SleepAnalysis (category) | SleepSessionRecord |
| Hydration | liters | DietaryWater | HydrationRecord |
Usage Examples
Request Permissions and Query Data
IHealthService health; // inject via DI
// Request read permissions for the data types you need
var result = await health.RequestPermissions(
DataType.StepCount,
DataType.HeartRate,
DataType.Calories,
DataType.Distance
);
// Or request per-metric read/write permissions in a single call
var result2 = await health.RequestPermissions(
(PermissionType.Read, DataType.StepCount),
(PermissionType.Read, DataType.HeartRate),
(PermissionType.Write, DataType.Weight),
(PermissionType.ReadWrite, DataType.BloodPressure)
);
// Check which permissions were granted
foreach (var (type, success) in result)
{
if (!success)
Console.WriteLine($"Permission denied for {type}");
}
// Query data for the last 24 hours, bucketed by day
var end = DateTimeOffset.Now;
var start = end.AddDays(-1);
var steps = (await health.GetStepCounts(start, end, Interval.Days)).Sum(x => x.Value);
var calories = (await health.GetCalories(start, end, Interval.Days)).Sum(x => x.Value);
var distance = (await health.GetDistances(start, end, Interval.Days)).Sum(x => x.Value);
var heartRate = (await health.GetAverageHeartRate(start, end, Interval.Days)).Average(x => x.Value);
Query Body Metrics
var weight = (await health.GetWeight(start, end, Interval.Days)).Average(x => x.Value); // kg
var height = (await health.GetHeight(start, end, Interval.Days)).Average(x => x.Value); // meters
var bodyFat = (await health.GetBodyFatPercentage(start, end, Interval.Days)).Average(x => x.Value); // %
var restingHr = (await health.GetRestingHeartRate(start, end, Interval.Days)).Average(x => x.Value); // bpm
Query Vitals
// Blood pressure returns BloodPressureResult with Systolic and Diastolic
var bp = await health.GetBloodPressure(start, end, Interval.Days);
if (bp.Any())
{
var avgSystolic = bp.Average(x => x.Systolic); // mmHg
var avgDiastolic = bp.Average(x => x.Diastolic); // mmHg
}
var o2 = (await health.GetOxygenSaturation(start, end, Interval.Days)).Average(x => x.Value); // %
Query Lifestyle
var sleep = (await health.GetSleepDuration(start, end, Interval.Days)).Sum(x => x.Value); // hours
var water = (await health.GetHydration(start, end, Interval.Days)).Sum(x => x.Value); // liters
Hourly Breakdown
// Get hourly step counts for the past week
var weekStart = DateTimeOffset.Now.AddDays(-7);
var weekEnd = DateTimeOffset.Now;
var hourlySteps = await health.GetStepCounts(weekStart, weekEnd, Interval.Hours);
foreach (var bucket in hourlySteps)
{
Console.WriteLine($"{bucket.Start:g} - {bucket.End:g}: {bucket.Value:N0} steps");
}
ViewModel Pattern (with CommunityToolkit.Mvvm)
public partial class HealthDashboardViewModel(IHealthService health) : ObservableObject
{
[ObservableProperty]
double steps;
[ObservableProperty]
double calories;
[RelayCommand]
async Task LoadDataAsync()
{
await health.RequestPermissions(DataType.StepCount, DataType.Calories);
var start = DateTimeOffset.Now.Date;
var end = DateTimeOffset.Now;
Steps = (await health.GetStepCounts(start, end, Interval.Days)).Sum(x => x.Value);
Calories = (await health.GetCalories(start, end, Interval.Days)).Sum(x => x.Value);
}
}
Writing Health Data
IHealthService health; // inject via DI
// Request write permissions for the data types you need
await health.RequestPermissions(PermissionType.Write, DataType.Weight, DataType.StepCount, DataType.Hydration);
// Or request both read and write at once
await health.RequestPermissions(PermissionType.ReadWrite, DataType.Weight);
// Or mix read/write per metric in a single call
await health.RequestPermissions(
(PermissionType.Write, DataType.Weight),
(PermissionType.Write, DataType.StepCount),
(PermissionType.ReadWrite, DataType.Hydration)
);
var now = DateTimeOffset.Now;
// Write a weight measurement (point-in-time: Start == End)
await health.Write(new NumericHealthResult(DataType.Weight, now, now, 75.0)); // kg
// Write step counts over a time range
await health.Write(new NumericHealthResult(DataType.StepCount, now.AddMinutes(-30), now, 500));
// Write hydration
await health.Write(new NumericHealthResult(DataType.Hydration, now.AddHours(-1), now, 0.5)); // liters
// Write blood pressure
await health.Write(new BloodPressureResult(now, now, 120.0, 80.0)); // mmHg
// Write sleep session
var sleepStart = now.AddHours(-8);
await health.Write(new NumericHealthResult(DataType.SleepDuration, sleepStart, now, 0)); // Value is ignored, duration derived from Start/End
Observing Real-Time Health Data
IHealthService health; // inject via DI
// Request read permission for the data type you want to observe
await health.RequestPermissions(DataType.StepCount);
// Observe step count changes in real time using IAsyncEnumerable
// Use a CancellationTokenSource to stop observation when done
using var cts = new CancellationTokenSource();
await foreach (var result in health.Observe(DataType.StepCount, cancelToken: cts.Token))
{
// result is a HealthResult — cast to NumericHealthResult for value
if (result is NumericHealthResult numeric)
Console.WriteLine($"Steps: {numeric.Value} ({numeric.Start:t} - {numeric.End:t})");
}
// On Android, you can customize the polling interval (default 5s, ignored on iOS)
await foreach (var result in health.Observe(DataType.HeartRate, pollingInterval: TimeSpan.FromSeconds(10), cancelToken: cts.Token))
{
if (result is NumericHealthResult numeric)
Console.WriteLine($"Heart rate: {numeric.Value} bpm");
}
Platform Notes
iOS
ObserveusesHKAnchoredObjectQueryfor push-based real-time updates (no polling needed)- HealthKit requires a real device (not simulator) for most data types
RequestPermissionson iOS does NOT tell you if the user denied access (Apple privacy policy) - it may returntrueeven when denied- Sleep data uses
HKCategoryTypeIdentifier.SleepAnalysis(category type, not quantity type) - the library handles this internally - Blood pressure requires permissions for both systolic and diastolic types - the library handles this automatically
- Percentage values (body fat, O2 saturation) are returned as 0-100, not 0-1
Android
Observeuses Health Connect change tokens with polling (default 5s interval, configurable viapollingIntervalparameter)- The Health Connect app must be installed on the device
- Body fat percentage and oxygen saturation use
ReadRecordsinstead of aggregate queries (Health Connect does not provide aggregate metrics for these types) - Sleep duration uses
SleepSessionRecord.SleepDurationTotalaggregate metric, returning hours - Blood pressure uses
BloodPressureRecord.SystolicAvgandDiastolicAvgaggregate metrics - All Kotlin coroutine interop is handled internally via
IContinuationbridge (AOT-safe, no reflection)
Best Practices
- Always request permissions first - Call
RequestPermissionsbefore reading or writing data. UsePermissionType.WriteorPermissionType.ReadWritewhen writing - Use appropriate intervals - Use
Interval.Daysfor summaries,Interval.Hoursfor detailed breakdowns - Handle empty results - Check
.Any()before calling.Average()to avoidInvalidOperationException - Use CancellationToken - Pass cancellation tokens for long-running queries
- Sum vs Average - Use
.Sum()for cumulative metrics (steps, calories, distance, hydration, sleep) and.Average()for point-in-time metrics (heart rate, weight, height, body fat, O2 sat, resting HR) - Blood pressure is special - It returns
BloodPressureResult(notNumericHealthResult) with separateSystolicandDiastolicvalues - Register early - Call
AddHealthIntegration()inMauiProgram.csduring app startup
Common Packages
dotnet add package Shiny.Health # Core health data library
dotnet add package Shiny.Core # Required dependency