By the end of this lesson, you will:
Step counting is the gateway drug of fitness tracking. Apps like Apple Fitness, Fitbit, and Strava turn walking into a game with goals, streaks, and leaderboards. When users hit 10,000 steps, they screenshot their progress ring and share it.
The psychology is simple: make goals visible, celebrate wins, and create friendly competition. In this lesson, we'll build step tracking features that users check daily and share proudly.
Let users customize their fitness targets:
import AsyncStorage from '@react-native-async-storage/async-storage';
const GOAL_KEY = '@step_goal';
const DEFAULT_GOAL = 10000;
export const getStepGoal = async () => {
try {
const goal = await AsyncStorage.getItem(GOAL_KEY);
return goal ? parseInt(goal, 10) : DEFAULT_GOAL;
} catch (error) {
console.error('Get goal error:', error);
return DEFAULT_GOAL;
}
};
export const setStepGoal = async (goal) => {
try {
await AsyncStorage.setItem(GOAL_KEY, goal.toString());
console.log('Goal updated:', goal);
return true;
} catch (error) {
console.error('Set goal error:', error);
return false;
}
};
// Goal presets for easy selection
export const GOAL_PRESETS = [
{ label: 'Light', value: 5000, icon: '🚶' },
{ label: 'Moderate', value: 10000, icon: '🏃' },
{ label: 'Active', value: 15000, icon: '💪' },
{ label: 'Extreme', value: 20000, icon: '🔥' },
];
Track daily progress toward goals:
export const calculateProgress = (currentSteps, goal) => {
const percentage = Math.min((currentSteps / goal) * 100, 100);
const remaining = Math.max(goal - currentSteps, 0);
const exceeded = currentSteps > goal;
return {
percentage: Math.round(percentage),
remaining,
exceeded,
completed: currentSteps >= goal,
};
};
// Usage
const progress = calculateProgress(7234, 10000);
console.log(progress);
// {
// percentage: 72,
// remaining: 2766,
// exceeded: false,
// completed: false
// }
Count consecutive days of goal achievement:
import AsyncStorage from '@react-native-async-storage/async-storage';
const STREAK_KEY = '@step_streak';
const LAST_COMPLETION_KEY = '@last_completion';
export const updateStreak = async (stepsToday, goal) => {
try {
const goalCompleted = stepsToday >= goal;
if (!goalCompleted) {
// Don't update streak if goal not met
return await getStreak();
}
const today = new Date().toDateString();
const lastCompletion = await AsyncStorage.getItem(LAST_COMPLETION_KEY);
if (lastCompletion === today) {
// Already recorded today
return await getStreak();
}
const currentStreak = await getStreak();
if (lastCompletion) {
const lastDate = new Date(lastCompletion);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (lastDate.toDateString() === yesterday.toDateString()) {
// Consecutive day - increment streak
const newStreak = currentStreak + 1;
await AsyncStorage.setItem(STREAK_KEY, newStreak.toString());
await AsyncStorage.setItem(LAST_COMPLETION_KEY, today);
return newStreak;
} else {
// Streak broken - reset to 1
await AsyncStorage.setItem(STREAK_KEY, '1');
await AsyncStorage.setItem(LAST_COMPLETION_KEY, today);
return 1;
}
} else {
// First time - start streak
await AsyncStorage.setItem(STREAK_KEY, '1');
await AsyncStorage.setItem(LAST_COMPLETION_KEY, today);
return 1;
}
} catch (error) {
console.error('Update streak error:', error);
return 0;
}
};
export const getStreak = async () => {
try {
const streak = await AsyncStorage.getItem(STREAK_KEY);
return streak ? parseInt(streak, 10) : 0;
} catch (error) {
return 0;
}
};
Award badges for milestones:
export const ACHIEVEMENTS = [
{
id: 'first_goal',
title: 'First Steps',
description: 'Complete your first daily goal',
icon: '🎯',
requirement: (stats) => stats.goalsCompleted >= 1,
},
{
id: 'week_streak',
title: 'Week Warrior',
description: '7-day streak',
icon: '🔥',
requirement: (stats) => stats.streak >= 7,
},
{
id: 'marathoner',
title: 'Marathoner',
description: 'Walk 100,000 steps in a month',
icon: '🏃',
requirement: (stats) => stats.monthlySteps >= 100000,
},
{
id: 'overachiever',
title: 'Overachiever',
description: 'Exceed goal by 50% in one day',
icon: '⭐',
requirement: (stats) => stats.todaySteps >= stats.goal * 1.5,
},
{
id: 'consistent',
title: 'Consistency King',
description: '30-day streak',
icon: '👑',
requirement: (stats) => stats.streak >= 30,
},
];
export const checkAchievements = (stats) => {
return ACHIEVEMENTS.filter((achievement) => achievement.requirement(stats));
};
Create circular progress indicators:
import { View, StyleSheet } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
export default function ProgressRing({ progress, size = 200, strokeWidth = 20 }) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (progress / 100) * circumference;
return (
<View style={[styles.container, { width: size, height: size }]}>
<Svg width={size} height={size}>
{/* Background circle */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#E0E0E0"
strokeWidth={strokeWidth}
fill="none"
/>
{/* Progress circle */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#4CAF50"
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</Svg>
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
});
Display step history:
import { View, Text, StyleSheet } from 'react-native';
import { BarChart } from 'react-native-chart-kit';
export default function WeeklyStepsChart({ data }) {
// data: [{ date: 'Mon', steps: 8234 }, ...]
const chartData = {
labels: data.map((d) => d.date.substring(0, 3)), // Mon, Tue, Wed...
datasets: [
{
data: data.map((d) => d.steps),
},
],
};
return (
<View style={styles.container}>
<Text style={styles.title}>This Week</Text>
<BarChart
data={chartData}
width={350}
height={220}
yAxisSuffix=" k"
yAxisLabel=""
chartConfig={{
backgroundColor: '#fff',
backgroundGradientFrom: '#fff',
backgroundGradientTo: '#fff',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(76, 175, 80, ${opacity})`,
labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
style: {
borderRadius: 16,
},
propsForLabels: {
fontSize: 12,
},
}}
style={styles.chart}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginVertical: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 12,
},
chart: {
borderRadius: 16,
},
});
Implement background step counting:
import { useEffect, useState } from 'react';
import { readRecords } from 'react-native-health-connect';
import { AppState } from 'react-native';
export default function RealTimeStepCounter() {
const [steps, setSteps] = useState(0);
const [lastUpdate, setLastUpdate] = useState(new Date());
useEffect(() => {
// Initial load
updateSteps();
// Update when app comes to foreground
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active') {
updateSteps();
}
});
// Periodic updates while app is active
const interval = setInterval(() => {
if (AppState.currentState === 'active') {
updateSteps();
}
}, 60000); // Every minute
return () => {
subscription.remove();
clearInterval(interval);
};
}, []);
const updateSteps = async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const now = new Date();
try {
const result = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
const totalSteps = result.records.reduce(
(sum, record) => sum + record.count,
0
);
setSteps(totalSteps);
setLastUpdate(new Date());
} catch (error) {
console.error('Update steps error:', error);
}
};
return { steps, lastUpdate, refresh: updateSteps };
}
Compare steps with friends:
export const calculateLeaderboard = (users) => {
// Sort by steps descending
const sorted = users
.map((user) => ({
...user,
rank: 0, // Will be calculated
}))
.sort((a, b) => b.steps - a.steps);
// Assign ranks
sorted.forEach((user, index) => {
user.rank = index + 1;
});
return sorted;
};
// Usage
const leaderboard = calculateLeaderboard([
{ id: '1', name: 'Alice', steps: 12340, avatar: '👩' },
{ id: '2', name: 'Bob', steps: 10230, avatar: '👨' },
{ id: '3', name: 'Charlie', steps: 14500, avatar: '🧑' },
]);
Complete step tracking app with goals and streaks:
import { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { readRecords, requestPermission } from 'react-native-health-connect';
export default function StepTrackerScreen() {
const [steps, setSteps] = useState(0);
const [goal, setGoal] = useState(10000);
const [streak, setStreak] = useState(0);
const [achievements, setAchievements] = useState([]);
useEffect(() => {
initializeTracker();
}, []);
const initializeTracker = async () => {
const permissions = [{ accessType: 'read', recordType: 'Steps' }];
const granted = await requestPermission(permissions);
if (granted) {
loadGoal();
loadStreak();
loadTodaySteps();
}
};
const loadGoal = async () => {
const savedGoal = await getStepGoal();
setGoal(savedGoal);
};
const loadStreak = async () => {
const currentStreak = await getStreak();
setStreak(currentStreak);
};
const loadTodaySteps = async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const now = new Date();
try {
const result = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: now.toISOString(),
},
});
const totalSteps = result.records.reduce(
(sum, record) => sum + record.count,
0
);
setSteps(totalSteps);
// Check for goal completion
if (totalSteps >= goal) {
const newStreak = await updateStreak(totalSteps, goal);
setStreak(newStreak);
// Check achievements
const stats = {
todaySteps: totalSteps,
goal,
streak: newStreak,
goalsCompleted: newStreak,
monthlySteps: totalSteps, // Simplified
};
const earned = checkAchievements(stats);
setAchievements(earned);
}
} catch (error) {
console.error('Load steps error:', error);
}
};
const progress = calculateProgress(steps, goal);
const handleGoalChange = () => {
Alert.alert(
'Set Goal',
'Choose your daily step goal',
GOAL_PRESETS.map((preset) => ({
text: `${preset.icon} ${preset.label} (${preset.value.toLocaleString()})`,
onPress: async () => {
await setStepGoal(preset.value);
setGoal(preset.value);
},
}))
);
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Step Tracker</Text>
{/* Progress Ring */}
<View style={styles.ringContainer}>
<ProgressRing progress={progress.percentage} size={250} strokeWidth={25} />
<View style={styles.ringCenter}>
<Text style={styles.stepsValue}>{steps.toLocaleString()}</Text>
<Text style={styles.stepsLabel}>steps</Text>
</View>
</View>
{/* Goal Status */}
<View style={styles.statusCard}>
{progress.completed ? (
<>
<Text style={styles.statusEmoji}>🎉</Text>
<Text style={styles.statusText}>Goal Reached!</Text>
{progress.exceeded && (
<Text style={styles.statusSubtext}>
{(steps - goal).toLocaleString()} steps over goal
</Text>
)}
</>
) : (
<>
<Text style={styles.statusEmoji}>🏃</Text>
<Text style={styles.statusText}>Keep Going!</Text>
<Text style={styles.statusSubtext}>
{progress.remaining.toLocaleString()} steps remaining
</Text>
</>
)}
</View>
{/* Streak */}
<TouchableOpacity style={styles.streakCard}>
<Text style={styles.streakEmoji}>🔥</Text>
<View style={styles.streakInfo}>
<Text style={styles.streakValue}>{streak}</Text>
<Text style={styles.streakLabel}>day streak</Text>
</View>
</TouchableOpacity>
{/* Goal Setting */}
<TouchableOpacity style={styles.goalButton} onPress={handleGoalChange}>
<Text style={styles.goalButtonText}>Change Goal ({goal.toLocaleString()})</Text>
</TouchableOpacity>
{/* Achievements */}
{achievements.length > 0 && (
<View style={styles.achievementsSection}>
<Text style={styles.achievementsTitle}>Achievements Earned</Text>
{achievements.map((achievement) => (
<View key={achievement.id} style={styles.achievementCard}>
<Text style={styles.achievementIcon}>{achievement.icon}</Text>
<View style={styles.achievementInfo}>
<Text style={styles.achievementTitle}>{achievement.title}</Text>
<Text style={styles.achievementDesc}>{achievement.description}</Text>
</View>
</View>
))}
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 30,
},
ringContainer: {
alignItems: 'center',
marginBottom: 30,
},
ringCenter: {
position: 'absolute',
top: '50%',
transform: [{ translateY: -40 }],
alignItems: 'center',
},
stepsValue: {
fontSize: 48,
fontWeight: 'bold',
color: '#4CAF50',
},
stepsLabel: {
fontSize: 18,
color: '#666',
},
statusCard: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
alignItems: 'center',
marginBottom: 20,
},
statusEmoji: {
fontSize: 48,
marginBottom: 12,
},
statusText: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
statusSubtext: {
fontSize: 16,
color: '#666',
},
streakCard: {
backgroundColor: '#FFF3E0',
borderRadius: 16,
padding: 20,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
streakEmoji: {
fontSize: 40,
marginRight: 16,
},
streakInfo: {
flex: 1,
},
streakValue: {
fontSize: 32,
fontWeight: 'bold',
},
streakLabel: {
fontSize: 16,
color: '#666',
},
goalButton: {
backgroundColor: '#007AFF',
borderRadius: 12,
padding: 16,
alignItems: 'center',
marginBottom: 20,
},
goalButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
achievementsSection: {
marginTop: 20,
},
achievementsTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 12,
},
achievementCard: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
achievementIcon: {
fontSize: 32,
marginRight: 16,
},
achievementInfo: {
flex: 1,
},
achievementTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
achievementDesc: {
fontSize: 14,
color: '#666',
},
});
| Pitfall | Solution |
|---|---|
| Steps not updating | Use AppState listener, refresh on app resume |
| Inaccurate streak | Check timezone issues, use consistent date formatting |
| Battery drain | Limit update frequency, use cached data when possible |
| Goal too easy/hard | Provide adaptive suggestions based on average steps |
| Missing context | Explain why goal isn't met (not enough data, permissions) |
In the next lesson, we'll explore Workout and Activity Logging, enabling users to track exercises, record workouts, and sync fitness data across apps.