Workshop: Mobile App (3 hours) β
Build a complete cross-platform mobile application with native features, backend integration, offline support, and app store deployment readiness. Create a fitness tracking app that showcases modern mobile development.
Workshop Overview β
Duration: 3 hours Difficulty: Intermediate Result: Deploy-ready mobile app
You'll build FitTrack - a fitness tracking app with:
- π± Cross-platform (iOS & Android)
- π Activity tracking with device sensors
- π Progress visualization
- π Offline sync
- πΈ Photo progress tracking
- π Push notifications
- π Backend integration
Prerequisites β
- Orchestre installed
- Node.js 18+
- Expo Go app on your phone
- 3 hours of focused time
Part 1: Setup & Planning (30 minutes) β
Initialize Mobile App β
/create fittrack react-native-expo
cd fittrackStrategic Planning β
/orchestrate "Build a fitness tracking mobile app with:
- Workout logging with exercise database
- Step counting and distance tracking
- Progress photos with before/after comparison
- Workout plans and schedules
- Social features for motivation
- Offline support with sync
- Push notifications for reminders"Backend Setup β
Create a backend API:
# In another directory
/create fittrack-api cloudflare-hono
# Then configure the mobile app to use it
/execute-task "Configure API client to connect to backend"Configure Development β
# Install dependencies
npm install
# Start development
npm startScan QR code with Expo Go to see on your device.
Part 2: Core Features (45 minutes) β
Navigation Structure β
/execute-task "Create bottom tab navigation with Home, Workouts, Progress, Profile screens"Creates navigation:
// navigation/AppNavigator.tsx
export function AppNavigator() {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: '#007AFF',
headerShown: false
}}
>
<Tab.Screen
name="Home"
component={HomeStack}
options={{
tabBarIcon: ({ color }) => (
<Ionicons name="home" size={24} color={color} />
)
}}
/>
<Tab.Screen
name="Workouts"
component={WorkoutStack}
options={{
tabBarIcon: ({ color }) => (
<Ionicons name="barbell" size={24} color={color} />
)
}}
/>
<Tab.Screen
name="Progress"
component={ProgressStack}
options={{
tabBarIcon: ({ color }) => (
<Ionicons name="trending-up" size={24} color={color} />
)
}}
/>
<Tab.Screen
name="Profile"
component={ProfileStack}
options={{
tabBarIcon: ({ color }) => (
<Ionicons name="person" size={24} color={color} />
)
}}
/>
</Tab.Navigator>
)
}Workout Tracking β
/add-screen "Workout logging with exercise selection and set tracking"Key implementation:
// screens/WorkoutScreen.tsx
export function WorkoutScreen() {
const [exercises, setExercises] = useState<Exercise[]>([])
const [isTracking, setIsTracking] = useState(false)
const startTime = useRef<Date>()
const startWorkout = () => {
setIsTracking(true)
startTime.current = new Date()
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
}
const addExercise = async () => {
const exercise = await navigateToExercisePicker()
setExercises([...exercises, exercise])
}
const completeWorkout = async () => {
const duration = Date.now() - startTime.current!.getTime()
const workout = {
exercises,
duration,
date: new Date(),
calories: calculateCalories(exercises, duration)
}
await saveWorkout(workout)
await syncWithBackend(workout)
// Show completion animation
showSuccessAnimation()
navigation.navigate('WorkoutSummary', { workout })
}
return (
<SafeAreaView style={styles.container}>
<WorkoutHeader
isTracking={isTracking}
duration={useTimer(startTime.current)}
/>
<ExerciseList
exercises={exercises}
onAddSet={(exerciseId, set) => addSet(exerciseId, set)}
onRemove={(exerciseId) => removeExercise(exerciseId)}
/>
<FloatingActionButton
onPress={isTracking ? completeWorkout : startWorkout}
label={isTracking ? 'Finish' : 'Start Workout'}
/>
</SafeAreaView>
)
}Exercise Database β
/execute-task "Create exercise database with categories and muscle groups"Part 3: Native Features (45 minutes) β
Step Tracking β
/execute-task "Integrate device pedometer for step counting with health permissions"Implementation:
// hooks/useStepCounter.ts
import { Pedometer } from 'expo-sensors'
export function useStepCounter() {
const [steps, setSteps] = useState(0)
const [isAvailable, setIsAvailable] = useState(false)
useEffect(() => {
checkAvailability()
if (Platform.OS === 'ios') {
// Request motion permissions
Pedometer.requestPermissionsAsync()
}
}, [])
const checkAvailability = async () => {
const available = await Pedometer.isAvailableAsync()
setIsAvailable(available)
if (available) {
// Get today's steps
const start = new Date()
start.setHours(0, 0, 0, 0)
const pastSteps = await Pedometer.getStepCountAsync(start, new Date())
setSteps(pastSteps.steps)
// Subscribe to live updates
const subscription = Pedometer.watchStepCount(result => {
setSteps(prevSteps => prevSteps + result.steps)
})
return () => subscription.remove()
}
}
return { steps, isAvailable }
}Progress Photos β
/execute-task "Add camera integration for progress photos with before/after comparison"Photo feature:
// components/ProgressPhoto.tsx
export function ProgressPhotoCapture() {
const [hasPermission, setHasPermission] = useState(false)
const [photos, setPhotos] = useState<ProgressPhoto[]>([])
const takePhoto = async () => {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
allowsEditing: true,
aspect: [3, 4]
})
if (!result.canceled) {
const photo = {
uri: result.assets[0].uri,
date: new Date(),
weight: currentWeight,
measurements: currentMeasurements
}
await saveProgressPhoto(photo)
setPhotos([...photos, photo])
}
}
const comparePhotos = (photo1: ProgressPhoto, photo2: ProgressPhoto) => {
navigation.navigate('PhotoComparison', {
before: photo1,
after: photo2
})
}
return (
<View style={styles.container}>
<PhotoGrid
photos={photos}
onPhotoPress={(photo) => setSelectedPhoto(photo)}
onCompare={comparePhotos}
/>
<ActionButton
icon="camera"
label="Take Progress Photo"
onPress={takePhoto}
/>
</View>
)
}Push Notifications β
/setup-push-notificationsNotification setup:
// services/notifications.ts
export class NotificationService {
async initialize() {
const { status } = await Notifications.requestPermissionsAsync()
if (status === 'granted') {
const token = await Notifications.getExpoPushTokenAsync()
await this.registerToken(token.data)
// Schedule workout reminders
await this.scheduleWorkoutReminders()
}
}
async scheduleWorkoutReminders() {
const workoutDays = await getUserWorkoutSchedule()
for (const day of workoutDays) {
await Notifications.scheduleNotificationAsync({
content: {
title: 'Time to Work Out! πͺ',
body: `Ready for ${day.workoutType}?`,
data: { screen: 'Workouts' }
},
trigger: {
weekday: day.weekday,
hour: day.hour,
minute: day.minute,
repeats: true
}
})
}
}
handleNotificationPress(notification: Notification) {
const screen = notification.request.content.data?.screen
if (screen) {
navigation.navigate(screen)
}
}
}Part 4: Offline Support (30 minutes) β
Data Persistence β
/add-offline-sync "Workout data with conflict resolution"Offline implementation:
// services/offline.ts
export class OfflineManager {
private db = new SQLite.SQLiteDatabase('fittrack.db')
private syncQueue: SyncItem[] = []
async saveWorkout(workout: Workout) {
// Save locally first
await this.db.executeSql(
'INSERT INTO workouts (id, data, synced) VALUES (?, ?, ?)',
[workout.id, JSON.stringify(workout), false]
)
// Try to sync
if (await isOnline()) {
await this.syncWorkout(workout)
} else {
this.syncQueue.push({ type: 'workout', data: workout })
}
}
async syncAll() {
const unsynced = await this.getUnsyncedItems()
for (const item of unsynced) {
try {
await this.syncItem(item)
await this.markSynced(item.id)
} catch (error) {
if (error.code === 'CONFLICT') {
await this.resolveConflict(item, error.serverData)
}
}
}
}
private async resolveConflict(local: SyncItem, server: any) {
// Last-write-wins strategy
if (local.updatedAt > server.updatedAt) {
await this.forceSync(local)
} else {
await this.updateLocal(server)
}
}
}Background Sync β
/execute-task "Implement background sync for offline data"Part 5: UI Polish (30 minutes) β
Animations β
/execute-task "Add smooth animations for transitions and interactions"Animation examples:
// components/AnimatedProgress.tsx
import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'
export function AnimatedProgressRing({ progress }: { progress: number }) {
const rotation = useSharedValue(0)
const scale = useSharedValue(0)
useEffect(() => {
scale.value = withSpring(1, { damping: 15 })
rotation.value = withSpring(progress * 360, { damping: 20 })
}, [progress])
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` }
]
}))
return (
<Animated.View style={[styles.progressRing, animatedStyle]}>
<Svg width={200} height={200}>
<Circle
cx={100}
cy={100}
r={90}
stroke="#e0e0e0"
strokeWidth={10}
fill="none"
/>
<AnimatedCircle
cx={100}
cy={100}
r={90}
stroke="#007AFF"
strokeWidth={10}
fill="none"
strokeDasharray={`${progress * 565.48} 565.48`}
/>
</Svg>
<Text style={styles.progressText}>{Math.round(progress * 100)}%</Text>
</Animated.View>
)
}Custom Components β
/execute-task "Create custom UI components for consistent design"Dark Mode β
/execute-task "Implement dark mode support with system preference detection"Part 6: Performance & Testing (30 minutes) β
Performance Optimization β
/performance-check --mobileOptimizations:
// Optimize image loading
const OptimizedImage = memo(({ uri, style }) => {
return (
<FastImage
style={style}
source={{
uri,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable
}}
resizeMode={FastImage.resizeMode.cover}
/>
)
})
// Optimize list rendering
const WorkoutList = () => {
const renderItem = useCallback(({ item }) => (
<WorkoutCard workout={item} />
), [])
const keyExtractor = useCallback((item) => item.id, [])
return (
<FlashList
data={workouts}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={120}
removeClippedSubviews
maxToRenderPerBatch={10}
windowSize={10}
/>
)
}Testing β
/execute-task "Create component tests and integration tests"Part 7: Deployment Prep (30 minutes) β
App Configuration β
Update app.json:
{
"expo": {
"name": "FitTrack",
"slug": "fittrack",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.fittrack",
"infoPlist": {
"NSCameraUsageDescription": "Used for progress photos",
"NSMotionUsageDescription": "Used for step counting"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.yourcompany.fittrack",
"permissions": ["CAMERA", "ACTIVITY_RECOGNITION"]
}
}
}Build for Testing β
# Build for iOS TestFlight
eas build --platform ios --profile preview
# Build for Android
eas build --platform android --profile previewApp Store Assets β
/execute-task "Generate app store screenshots and descriptions"Complete Features Showcase β
1. Workout Timer β
// components/WorkoutTimer.tsx
export function WorkoutTimer() {
const [seconds, setSeconds] = useState(0)
const [isActive, setIsActive] = useState(false)
useEffect(() => {
let interval = null
if (isActive) {
interval = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
}
return () => clearInterval(interval)
}, [isActive])
const formatTime = (totalSeconds: number) => {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const secs = totalSeconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes
.toString()
.padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return (
<View style={styles.timer}>
<Text style={styles.timerText}>{formatTime(seconds)}</Text>
<TouchableOpacity
onPress={() => setIsActive(!isActive)}
style={[styles.button, isActive && styles.activeButton]}
>
<Text style={styles.buttonText}>
{isActive ? 'Pause' : 'Start'}
</Text>
</TouchableOpacity>
</View>
)
}2. Exercise Form Videos β
// components/ExerciseGuide.tsx
export function ExerciseGuide({ exerciseId }) {
const exercise = useExercise(exerciseId)
return (
<ScrollView style={styles.container}>
<Video
source={{ uri: exercise.videoUrl }}
style={styles.video}
shouldPlay
isLooping
isMuted
/>
<View style={styles.content}>
<Text style={styles.title}>{exercise.name}</Text>
<Section title="Muscles Worked">
<MuscleChart muscles={exercise.muscles} />
</Section>
<Section title="Instructions">
{exercise.instructions.map((step, index) => (
<Step key={index} number={index + 1} text={step} />
))}
</Section>
<Section title="Common Mistakes">
<MistakesList mistakes={exercise.commonMistakes} />
</Section>
</View>
</ScrollView>
)
}3. Social Features β
// screens/SocialFeed.tsx
export function SocialFeed() {
const [posts, setPosts] = useState<WorkoutPost[]>([])
const shareWorkout = async (workout: Workout) => {
const post = await createPost({
type: 'workout',
workout,
caption: workoutCaption,
privacy: 'friends'
})
setPosts([post, ...posts])
showShareSuccess()
}
return (
<FlatList
data={posts}
renderItem={({ item }) => (
<WorkoutPost
post={item}
onLike={() => likePost(item.id)}
onComment={() => openComments(item.id)}
onShare={() => sharePost(item)}
/>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
/>
)
}Testing Your App β
Test Checklist β
- [ ] User registration and login
- [ ] Workout tracking flow
- [ ] Step counter accuracy
- [ ] Photo capture and storage
- [ ] Offline mode
- [ ] Push notifications
- [ ] Data sync
- [ ] Performance on older devices
Device Testing β
Test on:
- iPhone (latest and older)
- Android (various manufacturers)
- Different screen sizes
- Poor network conditions
Performance Targets β
- App Launch: <2s
- Screen Transitions: <300ms
- API Calls: <500ms
- Image Load: <1s
- Memory Usage: <150MB
What You've Built β
β Cross-platform mobile app β Native feature integration β Offline support β Backend connected β Push notifications β App store ready
Next Steps β
Your fitness app is ready! Consider:
- Beta Testing: Use TestFlight/Play Console
- Analytics: Add event tracking
- Monetization: Premium features or subscriptions
- Social: Build community features
- Wearables: Apple Watch/Wear OS support
- AI: Personalized workout recommendations
Resources β
Congratulations! You've built a production-ready mobile app in 3 hours!
