By the end of this lesson, you will:
Health and fitness apps create daily engagement loops. Apps like Strava, MyFitnessPal, and Peloton turn workouts into social experiences users share. When friends see your run streak, gym progress, or step count, it creates accountability and motivation.
Health Connect is Android's unified health platform, connecting apps to data from Fitbit, Samsung Health, Google Fit, and more. In this lesson, we'll build features that inspire users to move, track progress, and share achievements.
Health Connect is Android's centralized health and fitness data platform. It provides:
Key Benefits:
Install the React Native Health Connect library:
npm install react-native-health-connect
Configure Android permissions in AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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_SLEEP" />
<application>
<!-- Health Connect Activity -->
<activity
android:name="androidx.health.connect.client.PermissionController"
android:exported="true">
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
</application>
</manifest>
💡 Tip: Health Connect requires Android 9 (API 28) or higher. Gracefully handle unsupported devices.
Verify Health Connect is installed:
import {
initialize,
getSdkStatus,
SdkAvailabilityStatus,
} from 'react-native-health-connect';
import { useEffect, useState } from 'react';
import { Alert, Linking } from 'react-native';
export default function HealthConnectSetup() {
const [isAvailable, setIsAvailable] = useState(false);
useEffect(() => {
checkAvailability();
}, []);
const checkAvailability = async () => {
const status = await getSdkStatus();
switch (status) {
case SdkAvailabilityStatus.SDK_AVAILABLE:
await initialize();
setIsAvailable(true);
break;
case SdkAvailabilityStatus.SDK_UNAVAILABLE:
Alert.alert(
'Health Connect Required',
'Please install Health Connect from the Play Store',
[
{
text: 'Install',
onPress: () => {
Linking.openURL(
'https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata'
);
},
},
{ text: 'Cancel', style: 'cancel' },
]
);
break;
case SdkAvailabilityStatus.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED:
Alert.alert(
'Update Required',
'Health Connect needs to be updated'
);
break;
default:
console.log('Unknown Health Connect status');
}
};
return isAvailable;
}
Health data requires explicit user permission:
import { requestPermission, Permission } from 'react-native-health-connect';
import { Alert } from 'react-native';
export const requestHealthPermissions = async () => {
const permissions = [
{ accessType: 'read', recordType: 'Steps' },
{ accessType: 'read', recordType: 'HeartRate' },
{ accessType: 'read', recordType: 'Distance' },
{ accessType: 'read', recordType: 'TotalCaloriesBurned' },
{ accessType: 'read', recordType: 'SleepSession' },
];
try {
const granted = await requestPermission(permissions);
if (granted) {
console.log('Health permissions granted');
return true;
} else {
Alert.alert(
'Permissions Needed',
'Health data access is required to track your fitness progress'
);
return false;
}
} catch (error) {
console.error('Permission request error:', error);
return false;
}
};
Available Data Types:
Fetch daily step data:
import { readRecords } from 'react-native-health-connect';
export const getTodaySteps = async () => {
const today = new Date();
today.setHours(0, 0, 0, 0); // Start of day
const now = new Date();
try {
const result = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
// Sum all step records
const totalSteps = result.records.reduce(
(sum, record) => sum + record.count,
0
);
console.log(`Today's steps: ${totalSteps}`);
return totalSteps;
} catch (error) {
console.error('Read steps error:', error);
return 0;
}
};
Calculate weekly stats:
export const getWeeklySteps = async () => {
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
try {
const result = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: weekAgo.toISOString(),
endTime: now.toISOString(),
},
});
// Group by day
const dailySteps = {};
result.records.forEach((record) => {
const date = new Date(record.startTime).toLocaleDateString();
dailySteps[date] = (dailySteps[date] || 0) + record.count;
});
// Convert to array for charting
const weekData = Object.entries(dailySteps).map(([date, steps]) => ({
date,
steps,
}));
return weekData;
} catch (error) {
console.error('Read weekly steps error:', error);
return [];
}
};
Access heart rate measurements:
export const getRecentHeartRate = async () => {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
try {
const result = await readRecords('HeartRate', {
timeRangeFilter: {
operator: 'between',
startTime: oneHourAgo.toISOString(),
endTime: now.toISOString(),
},
});
if (result.records.length === 0) {
return null;
}
// Get latest measurement
const latest = result.records[result.records.length - 1];
return {
bpm: latest.beatsPerMinute,
time: new Date(latest.time),
};
} catch (error) {
console.error('Read heart rate error:', error);
return null;
}
};
Fetch sleep sessions:
export const getLastNightSleep = async () => {
const now = new Date();
now.setHours(12, 0, 0, 0); // Noon today
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(12, 0, 0, 0); // Noon yesterday
try {
const result = await readRecords('SleepSession', {
timeRangeFilter: {
operator: 'between',
startTime: yesterday.toISOString(),
endTime: now.toISOString(),
},
});
if (result.records.length === 0) {
return null;
}
const sleepSession = result.records[0];
const startTime = new Date(sleepSession.startTime);
const endTime = new Date(sleepSession.endTime);
const duration = (endTime - startTime) / (1000 * 60 * 60); // Hours
return {
startTime,
endTime,
duration: duration.toFixed(1),
stages: sleepSession.stages, // Deep, Light, REM, Awake
};
} catch (error) {
console.error('Read sleep error:', error);
return null;
}
};
Display comprehensive health metrics:
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView, RefreshControl } from 'react-native';
export default function HealthDashboard() {
const [steps, setSteps] = useState(0);
const [heartRate, setHeartRate] = useState(null);
const [sleep, setSleep] = useState(null);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadHealthData();
}, []);
const loadHealthData = async () => {
setRefreshing(true);
// Request permissions first
const hasPermission = await requestHealthPermissions();
if (!hasPermission) {
setRefreshing(false);
return;
}
// Load all data
const [stepsData, hrData, sleepData] = await Promise.all([
getTodaySteps(),
getRecentHeartRate(),
getLastNightSleep(),
]);
setSteps(stepsData);
setHeartRate(hrData);
setSleep(sleepData);
setRefreshing(false);
};
return (
<ScrollView
style={styles.container}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={loadHealthData} />
}
>
<Text style={styles.title}>Health Dashboard</Text>
{/* Steps Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Steps Today</Text>
<Text style={styles.cardValue}>{steps.toLocaleString()}</Text>
<Text style={styles.cardSubtitle}>
Goal: 10,000 ({Math.round((steps / 10000) * 100)}%)
</Text>
</View>
{/* Heart Rate Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Heart Rate</Text>
{heartRate ? (
<>
<Text style={styles.cardValue}>{heartRate.bpm} bpm</Text>
<Text style={styles.cardSubtitle}>
Updated {heartRate.time.toLocaleTimeString()}
</Text>
</>
) : (
<Text style={styles.cardSubtitle}>No recent data</Text>
)}
</View>
{/* Sleep Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Last Night's Sleep</Text>
{sleep ? (
<>
<Text style={styles.cardValue}>{sleep.duration} hours</Text>
<Text style={styles.cardSubtitle}>
{sleep.startTime.toLocaleTimeString()} -{' '}
{sleep.endTime.toLocaleTimeString()}
</Text>
</>
) : (
<Text style={styles.cardSubtitle}>No sleep data</Text>
)}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 20,
},
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
cardTitle: {
fontSize: 16,
color: '#666',
marginBottom: 8,
},
cardValue: {
fontSize: 36,
fontWeight: 'bold',
marginBottom: 4,
},
cardSubtitle: {
fontSize: 14,
color: '#999',
},
});
Handle health data responsibly:
export const HealthPrivacyGuidelines = {
// 1. Request only needed permissions
requestMinimalPermissions: () => {
// Only request data types you actually use
// Don't request "read all" permissions
},
// 2. Explain why you need data
showPermissionRationale: () => {
Alert.alert(
'Health Data Access',
'We use your step count to track daily goals and celebrate milestones. Your data stays private and is never shared without permission.'
);
},
// 3. Store health data securely
secureStorage: async (data) => {
// Use encrypted storage for sensitive health data
// await SecureStore.setItemAsync('health_data', JSON.stringify(data));
},
// 4. Never share without consent
shareData: async (data, recipient) => {
Alert.alert(
'Share Health Data?',
`Do you want to share your ${data.type} with ${recipient}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Share',
onPress: () => {
// Proceed with sharing
},
},
]
);
},
// 5. Provide data deletion
deleteUserData: async () => {
// Delete all cached health data
// await AsyncStorage.removeItem('health_cache');
},
};
Fitness tracking app with goal setting:
import { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ProgressBarAndroid,
} from 'react-native';
import { readRecords, requestPermission } from 'react-native-health-connect';
export default function FitnessTrackerScreen() {
const [steps, setSteps] = useState(0);
const [goal, setGoal] = useState(10000);
const [calories, setCalories] = useState(0);
const [distance, setDistance] = useState(0);
useEffect(() => {
setupHealthTracking();
}, []);
const setupHealthTracking = async () => {
const permissions = [
{ accessType: 'read', recordType: 'Steps' },
{ accessType: 'read', recordType: 'Distance' },
{ accessType: 'read', recordType: 'TotalCaloriesBurned' },
];
const granted = await requestPermission(permissions);
if (granted) {
loadTodayData();
}
};
const loadTodayData = async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const now = new Date();
try {
// Load steps
const stepsResult = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
const totalSteps = stepsResult.records.reduce(
(sum, record) => sum + record.count,
0
);
setSteps(totalSteps);
// Load distance
const distanceResult = await readRecords('Distance', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
const totalDistance = distanceResult.records.reduce(
(sum, record) => sum + record.distance.inMeters,
0
);
setDistance((totalDistance / 1000).toFixed(2)); // Convert to km
// Load calories
const caloriesResult = await readRecords('TotalCaloriesBurned', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
const totalCalories = caloriesResult.records.reduce(
(sum, record) => sum + record.energy.inKilocalories,
0
);
setCalories(Math.round(totalCalories));
} catch (error) {
console.error('Load data error:', error);
}
};
const progress = Math.min((steps / goal) * 100, 100);
const isGoalReached = steps >= goal;
return (
<View style={styles.container}>
<Text style={styles.title}>Today's Activity</Text>
{/* Steps Progress */}
<View style={styles.mainCard}>
<Text style={styles.mainValue}>{steps.toLocaleString()}</Text>
<Text style={styles.mainLabel}>steps</Text>
<ProgressBarAndroid
styleAttr="Horizontal"
indeterminate={false}
progress={progress / 100}
color="#4CAF50"
style={styles.progressBar}
/>
<Text style={styles.goalText}>
{isGoalReached
? `Goal Reached! 🎉`
: `${(goal - steps).toLocaleString()} steps to goal`}
</Text>
</View>
{/* Stats Grid */}
<View style={styles.statsGrid}>
<View style={styles.statCard}>
<Text style={styles.statValue}>{distance}</Text>
<Text style={styles.statLabel}>km</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{calories}</Text>
<Text style={styles.statLabel}>calories</Text>
</View>
</View>
<TouchableOpacity style={styles.refreshButton} onPress={loadTodayData}>
<Text style={styles.refreshButtonText}>Refresh Data</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 20,
},
mainCard: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 30,
alignItems: 'center',
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 5,
},
mainValue: {
fontSize: 56,
fontWeight: 'bold',
color: '#4CAF50',
},
mainLabel: {
fontSize: 20,
color: '#666',
marginBottom: 20,
},
progressBar: {
width: '100%',
height: 8,
marginVertical: 20,
},
goalText: {
fontSize: 16,
color: '#666',
},
statsGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20,
},
statCard: {
flex: 1,
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
alignItems: 'center',
marginHorizontal: 6,
},
statValue: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 4,
},
statLabel: {
fontSize: 14,
color: '#666',
},
refreshButton: {
backgroundColor: '#007AFF',
borderRadius: 12,
padding: 16,
alignItems: 'center',
},
refreshButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
| Pitfall | Solution |
|---|---|
| Health Connect not installed | Check availability, prompt user to install from Play Store |
| Permissions denied | Explain value clearly, allow app to work with limited data |
| Empty data | Handle gracefully, show onboarding for new users |
| Battery drain | Minimize read frequency, cache data, use aggregated queries |
| Privacy concerns | Be transparent, only request needed data, provide deletion option |
In the next lesson, we'll explore Step Tracking and Goals, building features that gamify fitness with daily challenges, streaks, and social competition.