Integrating HealthKit with Flutter

Hey there! I'm Pranav Masekar, a dedicated Flutter developer who's all about crafting captivating mobile apps that not only look stunning but also deliver unforgettable user experiences. But it doesn't stop there – I'm also a passionate blogger, sharing insights and best practices within the Flutter community. By contributing to the growth and knowledge-sharing of fellow developers, I'm committed to fostering a dynamic and collaborative environment. And let's not forget my DevOps journey – as a seasoned engineer, I've got the CI/CD pipelines, infrastructure-as-code, and cloud platforms down to an art.
Introduction
If you're a Flutter developer building health and fitness apps, you know the struggle! Getting access to native iOS health data like steps, calories, and heart rate can feel like navigating a maze of native code integration. But trust me, with Pigeon and a bit of Swift magic, we can make this happen seamlessly.
Prerequisites
Before we dive in, make sure you have these things ready:
Flutter SDK (3.9.0+)
Xcode with iOS Simulator or physical iPhone
Basic understanding of Flutter and iOS development
What is HealthKit?
HealthKit is Apple's framework for health and fitness data. It provides a centralised repository for health information from various sources like the iPhone, Apple Watch, and third-party apps. Think of it as the single source of truth for all your health metrics.
Pigeon is Flutter's code generation tool that creates type-safe communication between Flutter and native platforms. If you're new to Pigeon, check out my complete Pigeon guide for a deep dive into how it works!
Let's start with the Pigeon API definition
First things first, we need to define our API contract. Create a new file in the pigeons directory:
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/healthkit_api.dart',
dartOptions: DartOptions(),
kotlinOut:
'android/app/src/main/kotlin/com/pranav/iosPlayground/HealthKitApi.kt',
kotlinOptions: KotlinOptions(),
swiftOut: 'ios/Runner/HealthKitApi.swift',
swiftOptions: SwiftOptions(),
dartPackageName: 'com.pranav.iosPlayground',
),
)
class HealthSummary {
int steps;
double calories;
double avgHeartRate;
HealthSummary({
required this.steps,
required this.calories,
required this.avgHeartRate,
});
}
@HostApi()
abstract class HealthKitApi {
@async
HealthSummary getHealthSummary();
}
What's happening here:
We define a
HealthSummaryclass that holds our health metricsThe
@HostApi()annotation tells Pigeon this is our main API interface@asyncmakes the method asynchronous (obviously!)Pigeon will generate all the boilerplate code for us
Add Pigeon to your pubspec.yaml
Add Pigeon as a dev dependency:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
pigeon: ^26.0.1
Generate the platform code
Now here's the exciting part! Run this command to generate all the platform-specific code:
dart run pigeon --input pigeons/healthkit_api.dart
This single command generates:
Dart API client (
lib/healthkit_api.dart)Swift protocol and setup code (
ios/Runner/HealthKitApi.swift)Kotlin interface (for Android, though we're focusing on iOS here)
Implement the Swift HealthKit logic
Create the Swift implementation that actually talks to HealthKit:
import Foundation
import HealthKit
class HealthKitApiImplementation: HealthKitApi {
let store = HKHealthStore()
let typesToRead: Set = [
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!
]
func requestPermission(completion: @escaping (Bool) -> Void) {
store.requestAuthorization(toShare: [], read: typesToRead) { success, error in
if success {
print("Permission Approved")
completion(true)
} else {
print("Permission Rejected: \(String(describing: error))")
completion(false)
}
}
}
func getHealthSummary(completion: @escaping (Result<HealthSummary, any Error>) -> Void) {
requestPermission { granted in
guard granted else {
completion(.success(HealthSummary(steps: 0, calories: 0, avgHeartRate: 0)))
return
}
let startOfDay = Calendar.current.startOfDay(for: Date())
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
var steps: Double = 0
var calories: Double = 0
var heartRate: Double = 0
let group = DispatchGroup()
if let type = HKQuantityType.quantityType(forIdentifier: .stepCount) {
group.enter()
let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, _ in
steps = result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
group.leave()
}
self.store.execute(query)
}
if let type = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
group.enter()
let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, _ in
calories = result?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
group.leave()
}
self.store.execute(query)
}
if let type = HKQuantityType.quantityType(forIdentifier: .heartRate) {
group.enter()
let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .discreteAverage) { _, result, _ in
heartRate = result?.averageQuantity()?.doubleValue(for: HKUnit(from: "count/min")) ?? 0
group.leave()
}
self.store.execute(query)
}
group.notify(queue: .main) {
let summary = HealthSummary(
steps: Int64(steps),
calories: calories,
avgHeartRate: heartRate
)
print("Health summary:", summary)
completion(.success(summary))
}
}
}
}
Key points here:
We request permissions for steps, calories, and heart rate data
DispatchGrouphelps us wait for all three queries to completeWe query today's data using
predicateForSamplesEach query uses different options:
.cumulativeSumfor totals,.discreteAveragefor averages
Wire it into the iOS app
Update your AppDelegate.swift to register our implementation:
let healthKitApiImplementation = HealthKitApiImplementation()
let controller = window?.rootViewController as! FlutterViewController
HealthKitApiSetup.setUp(
binaryMessenger: controller.binaryMessenger,
api: healthKitApiImplementation
)
Add HealthKit permissions to Info.plist
Don't forget the privacy descriptions! Add these to your Info.plist:
<key>NSHealthShareUsageDescription</key>
<string>This app reads your Health data (steps, calories, heart rate) to show your activity summary.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This app updates your Health data when needed to keep your activity information accurate.</string>
Create the Flutter UI
Now let's build a beautiful UI to display our health data:
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'healthkit_api.dart';
class HomeView extends StatelessWidget {
const HomeView({super.key});
Future<HealthSummary> _loadSummary() {
final api = HealthKitApi();
return api.getHealthSummary();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
surfaceTintColor: Colors.transparent,
title: Text(
'Health Summary',
style: ShadTheme.of(context).textTheme.h2,
),
centerTitle: true,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: FutureBuilder<HealthSummary>(
future: _loadSummary(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(
'Failed to load health data',
style: ShadTheme.of(context).textTheme.muted,
),
);
}
final data = snapshot.data;
if (data == null) {
return Center(
child: Text(
'No data available',
style: ShadTheme.of(context).textTheme.muted,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 1.25,
children: [
_MetricCard(
title: 'Steps',
value: data.steps.toString(),
subtitle: 'Today',
),
_MetricCard(
title: 'Calories',
value: data.calories.toStringAsFixed(0),
subtitle: 'kcal',
),
_MetricCard(
title: 'Avg Heart Rate',
value: data.avgHeartRate.toStringAsFixed(0),
subtitle: 'bpm',
),
],
),
),
],
);
},
),
),
),
);
}
}
What's happening here:
We use
FutureBuilderto handle the async health data loadingThe UI shows loading, error, and success states (pro tip: always handle these!)
Grid layout displays our health metrics in beautiful cards
We format numbers appropriately (no decimals for steps, one decimal for heart rate)
Test it out!
Run your app on an iOS device or simulator:
flutter run
When you first launch the app, iOS will prompt you to grant HealthKit permissions. Once approved, you'll see your health data displayed in a clean, modern UI!
Wrap-up
So there you have it! We created a complete HealthKit integration using Pigeon for type-safe communication, implemented native Swift code to access health data, and built a beautiful Flutter UI to display it all.
You can find the complete source code for this project in the GitHub repository - feel free to fork it and experiment with your own health metrics!
Keep Fluttering 💙💙💙



