Skip to content

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 ​

bash
/create fittrack react-native-expo
cd fittrack

Strategic Planning ​

bash
/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:

bash
# 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 ​

bash
# Install dependencies
npm install

# Start development
npm start

Scan QR code with Expo Go to see on your device.

Part 2: Core Features (45 minutes) ​

bash
/execute-task "Create bottom tab navigation with Home, Workouts, Progress, Profile screens"

Creates navigation:

typescript
// 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 ​

bash
/add-screen "Workout logging with exercise selection and set tracking"

Key implementation:

typescript
// 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 ​

bash
/execute-task "Create exercise database with categories and muscle groups"

Part 3: Native Features (45 minutes) ​

Step Tracking ​

bash
/execute-task "Integrate device pedometer for step counting with health permissions"

Implementation:

typescript
// 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 ​

bash
/execute-task "Add camera integration for progress photos with before/after comparison"

Photo feature:

typescript
// 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 ​

bash
/setup-push-notifications

Notification setup:

typescript
// 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 ​

bash
/add-offline-sync "Workout data with conflict resolution"

Offline implementation:

typescript
// 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 ​

bash
/execute-task "Implement background sync for offline data"

Part 5: UI Polish (30 minutes) ​

Animations ​

bash
/execute-task "Add smooth animations for transitions and interactions"

Animation examples:

typescript
// 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 ​

bash
/execute-task "Create custom UI components for consistent design"

Dark Mode ​

bash
/execute-task "Implement dark mode support with system preference detection"

Part 6: Performance & Testing (30 minutes) ​

Performance Optimization ​

bash
/performance-check --mobile

Optimizations:

typescript
// 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 ​

bash
/execute-task "Create component tests and integration tests"

Part 7: Deployment Prep (30 minutes) ​

App Configuration ​

Update app.json:

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 ​

bash
# Build for iOS TestFlight
eas build --platform ios --profile preview

# Build for Android
eas build --platform android --profile preview

App Store Assets ​

bash
/execute-task "Generate app store screenshots and descriptions"

Complete Features Showcase ​

1. Workout Timer ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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:

  1. Beta Testing: Use TestFlight/Play Console
  2. Analytics: Add event tracking
  3. Monetization: Premium features or subscriptions
  4. Social: Build community features
  5. Wearables: Apple Watch/Wear OS support
  6. AI: Personalized workout recommendations

Resources ​

Congratulations! You've built a production-ready mobile app in 3 hours!

Built with ❀️ for the AI Coding community, by Praney Behl