Practice and reinforce the concepts from Lesson 11
Build a workout logging app that records exercise sessions with type, duration, intensity, and notes. Display workout history with statistics, provide workout templates for quick logging, and sync data with Health Connect.
A fitness tracking app where users can log various types of workouts (running, cycling, strength training), track metrics like duration and calories, view workout history with charts, and save favorite workouts as templates for easy re-logging.

Define the structure for workout data:
// models/Workout.js
export class Workout {
constructor(data = {}) {
this.id = data.id || Date.now().toString();
this.type = data.type || 'Running'; // Running, Cycling, Swimming, Strength, Yoga, etc.
this.startTime = data.startTime || new Date();
this.endTime = data.endTime || new Date();
this.duration = data.duration || 0; // minutes
this.intensity = data.intensity || 'Moderate'; // Light, Moderate, Vigorous
this.calories = data.calories || 0;
this.distance = data.distance || 0; // km
this.notes = data.notes || '';
this.heartRateAvg = data.heartRateAvg || null;
this.isTemplate = data.isTemplate || false;
}
getDuration() {
if (this.duration) return this.duration;
const diff = this.endTime - this.startTime;
return Math.round(diff / (1000 * 60));
}
getCaloriesBurned() {
if (this.calories) return this.calories;
// Estimate calories based on type and duration
const caloriesPerMinute = {
Running: 12,
Cycling: 8,
Swimming: 10,
Strength: 6,
Yoga: 3,
Walking: 4,
HIIT: 15,
};
const rate = caloriesPerMinute[this.type] || 5;
const intensityMultiplier = {
Light: 0.7,
Moderate: 1.0,
Vigorous: 1.3,
};
return Math.round(
this.getDuration() * rate * (intensityMultiplier[this.intensity] || 1)
);
}
toJSON() {
return {
id: this.id,
type: this.type,
startTime: this.startTime.toISOString(),
endTime: this.endTime.toISOString(),
duration: this.duration,
intensity: this.intensity,
calories: this.calories,
distance: this.distance,
notes: this.notes,
heartRateAvg: this.heartRateAvg,
isTemplate: this.isTemplate,
};
}
static fromJSON(json) {
return new Workout({
...json,
startTime: new Date(json.startTime),
endTime: new Date(json.endTime),
});
}
}
export const workoutTypes = [
{ id: 'Running', icon: '🏃', color: '#FF6B6B' },
{ id: 'Cycling', icon: '🚴', color: '#4ECDC4' },
{ id: 'Swimming', icon: '🏊', color: '#45B7D1' },
{ id: 'Strength', icon: '💪', color: '#FFA07A' },
{ id: 'Yoga', icon: '🧘', color: '#DDA15E' },
{ id: 'Walking', icon: '🚶', color: '#95E1D3' },
{ id: 'HIIT', icon: '⚡', color: '#F38181' },
];
Build a service to persist workout data:
// services/WorkoutStorage.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Workout } from '../models/Workout';
const WORKOUTS_KEY = '@workouts';
const TEMPLATES_KEY = '@workout_templates';
export class WorkoutStorage {
async saveWorkout(workout) {
try {
const workouts = await this.getAllWorkouts();
workouts.push(workout);
await AsyncStorage.setItem(WORKOUTS_KEY, JSON.stringify(workouts));
return true;
} catch (error) {
console.error('Error saving workout:', error);
return false;
}
}
async getAllWorkouts() {
try {
const data = await AsyncStorage.getItem(WORKOUTS_KEY);
if (!data) return [];
const workouts = JSON.parse(data);
return workouts.map(w => Workout.fromJSON(w));
} catch (error) {
console.error('Error loading workouts:', error);
return [];
}
}
async getWorkoutsByDateRange(startDate, endDate) {
const workouts = await this.getAllWorkouts();
return workouts.filter(w => {
const workoutDate = new Date(w.startTime);
return workoutDate >= startDate && workoutDate <= endDate;
});
}
async getWorkoutsByType(type) {
const workouts = await this.getAllWorkouts();
return workouts.filter(w => w.type === type);
}
async updateWorkout(workoutId, updates) {
try {
const workouts = await this.getAllWorkouts();
const index = workouts.findIndex(w => w.id === workoutId);
if (index !== -1) {
workouts[index] = new Workout({ ...workouts[index], ...updates });
await AsyncStorage.setItem(WORKOUTS_KEY, JSON.stringify(workouts));
return true;
}
return false;
} catch (error) {
console.error('Error updating workout:', error);
return false;
}
}
async deleteWorkout(workoutId) {
try {
const workouts = await this.getAllWorkouts();
const filtered = workouts.filter(w => w.id !== workoutId);
await AsyncStorage.setItem(WORKOUTS_KEY, JSON.stringify(filtered));
return true;
} catch (error) {
console.error('Error deleting workout:', error);
return false;
}
}
async saveTemplate(workout) {
try {
const templates = await this.getTemplates();
const template = new Workout({ ...workout, isTemplate: true });
templates.push(template);
await AsyncStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
return true;
} catch (error) {
console.error('Error saving template:', error);
return false;
}
}
async getTemplates() {
try {
const data = await AsyncStorage.getItem(TEMPLATES_KEY);
if (!data) return [];
const templates = JSON.parse(data);
return templates.map(t => Workout.fromJSON(t));
} catch (error) {
console.error('Error loading templates:', error);
return [];
}
}
async getStatistics() {
const workouts = await this.getAllWorkouts();
const totalWorkouts = workouts.length;
const totalMinutes = workouts.reduce((sum, w) => sum + w.getDuration(), 0);
const totalCalories = workouts.reduce((sum, w) => sum + w.getCaloriesBurned(), 0);
const totalDistance = workouts.reduce((sum, w) => sum + (w.distance || 0), 0);
const typeBreakdown = {};
workouts.forEach(w => {
typeBreakdown[w.type] = (typeBreakdown[w.type] || 0) + 1;
});
const thisWeek = await this.getWorkoutsByDateRange(
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
new Date()
);
return {
total: {
workouts: totalWorkouts,
minutes: totalMinutes,
calories: totalCalories,
distance: totalDistance.toFixed(2),
},
thisWeek: {
workouts: thisWeek.length,
minutes: thisWeek.reduce((sum, w) => sum + w.getDuration(), 0),
},
byType: typeBreakdown,
};
}
}
💡 Tip: Consider syncing workout data with Health Connect so it appears in other fitness apps. Use the writeRecords API to store exercise sessions.
Build a form for logging new workouts:
// components/WorkoutForm.js
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
Modal,
} from 'react-native';
import { workoutTypes } from '../models/Workout';
import DateTimePicker from '@react-native-community/datetimepicker';
const WorkoutForm = ({ visible, onSave, onCancel, initialData = {} }) => {
const [type, setType] = useState(initialData.type || 'Running');
const [startTime, setStartTime] = useState(initialData.startTime || new Date());
const [duration, setDuration] = useState(initialData.duration?.toString() || '30');
const [intensity, setIntensity] = useState(initialData.intensity || 'Moderate');
const [distance, setDistance] = useState(initialData.distance?.toString() || '');
const [notes, setNotes] = useState(initialData.notes || '');
const [showTimePicker, setShowTimePicker] = useState(false);
const intensityLevels = ['Light', 'Moderate', 'Vigorous'];
const handleSave = () => {
const workout = {
...initialData,
type,
startTime,
duration: parseInt(duration) || 0,
intensity,
distance: parseFloat(distance) || 0,
notes,
};
onSave(workout);
};
return (
<Modal visible={visible} animationType="slide">
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onCancel}>
<Text style={styles.cancelButton}>Cancel</Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Log Workout</Text>
<TouchableOpacity onPress={handleSave}>
<Text style={styles.saveButton}>Save</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.form}>
<Text style={styles.label}>Workout Type</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.typeScroll}
>
{workoutTypes.map((wType) => (
<TouchableOpacity
key={wType.id}
style={[
styles.typeButton,
type === wType.id && { backgroundColor: wType.color }
]}
onPress={() => setType(wType.id)}
>
<Text style={styles.typeIcon}>{wType.icon}</Text>
<Text
style={[
styles.typeLabel,
type === wType.id && styles.typeLabelSelected
]}
>
{wType.id}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<Text style={styles.label}>Start Time</Text>
<TouchableOpacity
style={styles.input}
onPress={() => setShowTimePicker(true)}
>
<Text>{startTime.toLocaleTimeString()}</Text>
</TouchableOpacity>
{showTimePicker && (
<DateTimePicker
value={startTime}
mode="time"
onChange={(event, selectedDate) => {
setShowTimePicker(false);
if (selectedDate) setStartTime(selectedDate);
}}
/>
)}
<Text style={styles.label}>Duration (minutes)</Text>
<TextInput
style={styles.input}
value={duration}
onChangeText={setDuration}
keyboardType="numeric"
placeholder="30"
/>
<Text style={styles.label}>Intensity</Text>
<View style={styles.intensityButtons}>
{intensityLevels.map((level) => (
<TouchableOpacity
key={level}
style={[
styles.intensityButton,
intensity === level && styles.intensityButtonSelected
]}
onPress={() => setIntensity(level)}
>
<Text
style={[
styles.intensityText,
intensity === level && styles.intensityTextSelected
]}
>
{level}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>Distance (km) - Optional</Text>
<TextInput
style={styles.input}
value={distance}
onChangeText={setDistance}
keyboardType="decimal-pad"
placeholder="5.0"
/>
<Text style={styles.label}>Notes</Text>
<TextInput
style={[styles.input, styles.notesInput]}
value={notes}
onChangeText={setNotes}
multiline
numberOfLines={4}
placeholder="How did it go?"
/>
</ScrollView>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
cancelButton: {
color: '#666',
fontSize: 16,
},
headerTitle: {
fontSize: 18,
fontWeight: 'bold',
},
saveButton: {
color: '#4ECDC4',
fontSize: 16,
fontWeight: 'bold',
},
form: {
padding: 20,
},
label: {
fontSize: 16,
fontWeight: '600',
marginTop: 20,
marginBottom: 10,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 10,
padding: 15,
fontSize: 16,
},
notesInput: {
height: 100,
textAlignVertical: 'top',
},
typeScroll: {
marginBottom: 10,
},
typeButton: {
backgroundColor: '#f5f5f5',
borderRadius: 15,
padding: 15,
alignItems: 'center',
marginRight: 10,
minWidth: 90,
},
typeIcon: {
fontSize: 32,
marginBottom: 8,
},
typeLabel: {
fontSize: 12,
color: '#666',
},
typeLabelSelected: {
color: 'white',
fontWeight: 'bold',
},
intensityButtons: {
flexDirection: 'row',
gap: 10,
},
intensityButton: {
flex: 1,
backgroundColor: '#f5f5f5',
borderRadius: 10,
padding: 15,
alignItems: 'center',
},
intensityButtonSelected: {
backgroundColor: '#4ECDC4',
},
intensityText: {
fontSize: 14,
color: '#666',
},
intensityTextSelected: {
color: 'white',
fontWeight: 'bold',
},
});
export default WorkoutForm;
Display past workouts in a scrollable list:
// components/WorkoutHistory.js
import React from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { workoutTypes } from '../models/Workout';
const WorkoutHistory = ({ workouts, onWorkoutPress }) => {
const getWorkoutColor = (type) => {
const workout = workoutTypes.find(w => w.id === type);
return workout?.color || '#4ECDC4';
};
const getWorkoutIcon = (type) => {
const workout = workoutTypes.find(w => w.id === type);
return workout?.icon || '🏋️';
};
const formatDate = (date) => {
const today = new Date();
const workoutDate = new Date(date);
if (workoutDate.toDateString() === today.toDateString()) {
return 'Today';
}
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (workoutDate.toDateString() === yesterday.toDateString()) {
return 'Yesterday';
}
return workoutDate.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
};
const renderWorkout = ({ item }) => (
<TouchableOpacity
style={styles.workoutCard}
onPress={() => onWorkoutPress(item)}
>
<View
style={[
styles.iconContainer,
{ backgroundColor: getWorkoutColor(item.type) }
]}
>
<Text style={styles.workoutIcon}>{getWorkoutIcon(item.type)}</Text>
</View>
<View style={styles.workoutDetails}>
<Text style={styles.workoutType}>{item.type}</Text>
<Text style={styles.workoutDate}>
{formatDate(item.startTime)} • {item.intensity}
</Text>
</View>
<View style={styles.workoutStats}>
<Text style={styles.statValue}>{item.getDuration()}</Text>
<Text style={styles.statLabel}>min</Text>
</View>
<View style={styles.workoutStats}>
<Text style={styles.statValue}>{item.getCaloriesBurned()}</Text>
<Text style={styles.statLabel}>cal</Text>
</View>
</TouchableOpacity>
);
if (workouts.length === 0) {
return (
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}>🏋️</Text>
<Text style={styles.emptyTitle}>No Workouts Yet</Text>
<Text style={styles.emptyText}>
Log your first workout to start tracking your fitness journey!
</Text>
</View>
);
}
return (
<FlatList
data={workouts}
renderItem={renderWorkout}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
);
};
const styles = StyleSheet.create({
list: {
padding: 15,
},
workoutCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
borderRadius: 15,
padding: 15,
marginBottom: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
iconContainer: {
width: 50,
height: 50,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
marginRight: 15,
},
workoutIcon: {
fontSize: 24,
},
workoutDetails: {
flex: 1,
},
workoutType: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
workoutDate: {
fontSize: 12,
color: '#666',
},
workoutStats: {
alignItems: 'center',
marginLeft: 15,
},
statValue: {
fontSize: 18,
fontWeight: 'bold',
},
statLabel: {
fontSize: 11,
color: '#666',
},
emptyState: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
emptyIcon: {
fontSize: 64,
marginBottom: 20,
},
emptyTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
},
emptyText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
});
export default WorkoutHistory;
Integrate all components:
// App.js
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { Workout } from './models/Workout';
import { WorkoutStorage } from './services/WorkoutStorage';
import WorkoutForm from './components/WorkoutForm';
import WorkoutHistory from './components/WorkoutHistory';
export default function App() {
const [showForm, setShowForm] = useState(false);
const [workouts, setWorkouts] = useState([]);
const [stats, setStats] = useState(null);
const [selectedWorkout, setSelectedWorkout] = useState(null);
const storage = new WorkoutStorage();
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
const allWorkouts = await storage.getAllWorkouts();
setWorkouts(allWorkouts.sort((a, b) => b.startTime - a.startTime));
const statistics = await storage.getStatistics();
setStats(statistics);
};
const handleSaveWorkout = async (workoutData) => {
const workout = new Workout(workoutData);
await storage.saveWorkout(workout);
await loadData();
setShowForm(false);
};
const handleWorkoutPress = (workout) => {
setSelectedWorkout(workout);
setShowForm(true);
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Workout Logger</Text>
<TouchableOpacity
style={styles.addButton}
onPress={() => {
setSelectedWorkout(null);
setShowForm(true);
}}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
{stats && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.statsScroll}
contentContainerStyle={styles.statsContainer}
>
<View style={styles.statCard}>
<Text style={styles.statValue}>{stats.total.workouts}</Text>
<Text style={styles.statLabel}>Total Workouts</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>
{Math.round(stats.total.minutes / 60)}h
</Text>
<Text style={styles.statLabel}>Time Exercised</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>
{stats.total.calories.toLocaleString()}
</Text>
<Text style={styles.statLabel}>Calories Burned</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{stats.thisWeek.workouts}</Text>
<Text style={styles.statLabel}>This Week</Text>
</View>
</ScrollView>
)}
<WorkoutHistory
workouts={workouts}
onWorkoutPress={handleWorkoutPress}
/>
<WorkoutForm
visible={showForm}
initialData={selectedWorkout}
onSave={handleSaveWorkout}
onCancel={() => setShowForm(false)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
backgroundColor: 'white',
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
},
addButton: {
backgroundColor: '#4ECDC4',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
},
addButtonText: {
fontSize: 32,
color: 'white',
lineHeight: 40,
},
statsScroll: {
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
statsContainer: {
padding: 15,
gap: 15,
},
statCard: {
backgroundColor: '#f5f5f5',
borderRadius: 15,
padding: 20,
alignItems: 'center',
minWidth: 120,
},
statValue: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 5,
},
statLabel: {
fontSize: 12,
color: '#666',
},
});
Problem: Workouts not saving Solution: Check AsyncStorage permissions. Verify JSON serialization works correctly. Use try-catch blocks around all storage operations.
Problem: Statistics showing zero despite logged workouts Solution: Ensure date filtering logic is correct. Check that workout duration calculations return valid numbers. Verify reduce operations don't have initial value issues.
Problem: Calorie estimates seem inaccurate Solution: Adjust calories-per-minute rates in Workout model. Consider adding user weight as a factor. Let users manually override calculated values.
Problem: Date picker not appearing Solution: Install @react-native-community/datetimepicker. Check platform-specific configuration. Ensure showTimePicker state toggles correctly.
Problem: Form doesn't clear after saving Solution: Reset all form state values after successful save. Create a resetForm function that sets all fields to defaults.
For advanced students:
GPS Tracking: Integrate location tracking to map running/cycling routes with elevation data
Rest Timer: Add countdown timer for rest periods between strength training sets
Workout Plans: Create multi-day workout programs with scheduled exercises
Social Sharing: Allow users to share workout achievements to social media
Export to CSV: Generate workout reports in CSV format for analysis in spreadsheets
In this activity, you:
In the next lesson, you'll explore freemium design patterns. You'll learn how to create compelling free features while designing premium upgrades that users want to purchase.