Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
daffy0208 avatar

Mobile Developer

  • 399 installs
  • 33 repo stars
  • Updated December 25, 2025
  • daffy0208/ai-dev-standards

mobile-developer is an agent skill that helps developers build React Native and Expo cross-platform iOS and Android apps with navigation, native features, and store distribution.

About

mobile-developer is a version 1.0.0 agent skill from daffy0208/ai-dev-standards for React Native and Expo cross-platform mobile development. It scaffolds TypeScript Expo apps with Expo Router tab and stack navigation, reusable Button and Card components, Zustand auth stores, and hooks for fetch, camera, location, and push notifications via expo-camera, expo-location, and expo-notifications. The skill also covers FlashList list optimization, expo-image caching, EAS build and submit commands for App Store and Google Play, and OTA updates with eas update. Developers reach for mobile-developer when standing up iOS and Android from one codebase, wiring native device APIs, optimizing bundle and list performance, or preparing TestFlight and Play Store releases. Jest and React Native Testing Library examples are included for component tests.

  • Screen and navigation scaffolding
  • Platform UI and gesture patterns
  • API client integration
  • Offline and performance basics
  • Store build and signing prep

Mobile Developer by the numbers

  • 399 all-time installs (skills.sh)
  • Ranked #335 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daffy0208/ai-dev-standards --skill mobile-developer

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs399
repo stars33
Last updatedDecember 25, 2025
Repositorydaffy0208/ai-dev-standards

How do you build a React Native Expo app?

Implement native or cross-platform screens, navigation, offline behavior, and store-ready builds while integrating mobile APIs and platform SDKs.

Who is it for?

Developers starting or extending Expo/React Native apps who need navigation, native APIs, performance patterns, and store submission guidance in one skill.

Skip if: Native Swift/Kotlin-only projects without React Native—this skill centers on Expo and JavaScript/TypeScript cross-platform stacks.

When should I use this skill?

A developer asks to create an Expo app, add camera/location/push notifications, configure Expo Router tabs, or run EAS builds for iOS or Android.

What you get

Expo project structure, navigation layouts, UI components, native feature hooks, app.json config, and EAS build or submit command sequences.

  • Expo app scaffold
  • Navigation and component code
  • EAS deployment commands

By the numbers

  • Skill version 1.0.0 in manifest metadata

Files

SKILL.mdMarkdownGitHub ↗

Mobile Developer Skill

I help you build cross-platform mobile apps with React Native and Expo.

What I Do

App Development:

  • React Native / Expo apps (iOS + Android)
  • Navigation and routing
  • State management
  • API integration

Native Features:

  • Camera, location, notifications
  • Biometric authentication
  • File system access
  • Device sensors

Performance:

  • Optimize bundle size
  • Lazy loading
  • Image optimization
  • Memory management

Distribution:

  • App Store / Google Play submission
  • Over-the-air (OTA) updates
  • Beta testing (TestFlight, internal testing)

Quick Start: Expo App

Create New App

# Create Expo app
npx create-expo-app my-app --template blank-typescript

cd my-app

# Install dependencies
npx expo install react-native-screens react-native-safe-area-context
npx expo install expo-router

# Start development
npx expo start

Project Structure

my-app/
├── app/
│   ├── (tabs)/
│   │   ├── index.tsx          # Home tab
│   │   ├── profile.tsx        # Profile tab
│   │   └── _layout.tsx        # Tab layout
│   ├── users/
│   │   └── [id].tsx          # Dynamic route
│   ├── _layout.tsx           # Root layout
│   └── +not-found.tsx        # 404 page
├── components/
│   ├── Button.tsx
│   ├── Card.tsx
│   └── Loading.tsx
├── hooks/
│   └── useAuth.ts
├── app.json
└── package.json

---

Navigation with Expo Router

Tab Navigation

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#007AFF',
        headerShown: false
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          )
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          )
        }}
      />
    </Tabs>
  )
}

Stack Navigation

// app/users/[id].tsx
import { useLocalSearchParams } from 'expo-router'
import { View, Text } from 'react-native'

export default function UserDetail() {
  const { id } = useLocalSearchParams()

  return (
    <View>
      <Text>User ID: {id}</Text>
    </View>
  )
}

---

UI Components

Custom Button

// components/Button.tsx
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'

interface ButtonProps {
  title: string
  onPress: () => void
  variant?: 'primary' | 'secondary'
  loading?: boolean
  disabled?: boolean
}

export function Button({
  title,
  onPress,
  variant = 'primary',
  loading = false,
  disabled = false
}: ButtonProps) {
  return (
    <TouchableOpacity
      style={[
        styles.button,
        variant === 'primary' ? styles.primary : styles.secondary,
        disabled && styles.disabled
      ]}
      onPress={onPress}
      disabled={disabled || loading}
      activeOpacity={0.7}
    >
      {loading ? (
        <ActivityIndicator color="#fff" />
      ) : (
        <Text style={styles.text}>{title}</Text>
      )}
    </TouchableOpacity>
  )
}

const styles = StyleSheet.create({
  button: {
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center'
  },
  primary: {
    backgroundColor: '#007AFF'
  },
  secondary: {
    backgroundColor: '#8E8E93'
  },
  disabled: {
    opacity: 0.5
  },
  text: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600'
  }
})

Card Component

// components/Card.tsx
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'
import { ReactNode } from 'react'

interface CardProps {
  title?: string
  children: ReactNode
  onPress?: () => void
}

export function Card({ title, children, onPress }: CardProps) {
  const Container = onPress ? TouchableOpacity : View

  return (
    <Container
      style={styles.card}
      onPress={onPress}
      activeOpacity={onPress ? 0.7 : 1}
    >
      {title && <Text style={styles.title}>{title}</Text>}
      {children}
    </Container>
  )
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 12
  }
})

---

Data Fetching

Custom Hook

// hooks/useQuery.ts
import { useState, useEffect } from 'react'

interface UseQueryResult<T> {
  data: T | null
  loading: boolean
  error: Error | null
  refetch: () => void
}

export function useQuery<T>(url: string): UseQueryResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  const fetchData = async () => {
    try {
      setLoading(true)
      const response = await fetch(url)

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const json = await response.json()
      setData(json)
      setError(null)
    } catch (e) {
      setError(e as Error)
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchData()
  }, [url])

  return { data, loading, error, refetch: fetchData }
}

Usage

// app/(tabs)/index.tsx
import { View, Text, FlatList, RefreshControl } from 'react-native'
import { useQuery } from '@/hooks/useQuery'
import { Card } from '@/components/Card'

interface Post {
  id: string
  title: string
  content: string
}

export default function HomeScreen() {
  const { data, loading, error, refetch } = useQuery<Post[]>(
    'https://api.example.com/posts'
  )

  if (error) {
    return (
      <View>
        <Text>Error: {error.message}</Text>
      </View>
    )
  }

  return (
    <FlatList
      data={data || []}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Card title={item.title}>
          <Text>{item.content}</Text>
        </Card>
      )}
      refreshControl={
        <RefreshControl refreshing={loading} onRefresh={refetch} />
      }
    />
  )
}

---

Native Features

Camera

// app/camera.tsx
import { Camera, CameraType } from 'expo-camera'
import { useState } from 'react'
import { Button, View, StyleSheet } from 'react-native'

export default function CameraScreen() {
  const [type, setType] = useState(CameraType.back)
  const [permission, requestPermission] = Camera.useCameraPermissions()

  if (!permission) {
    return <View />
  }

  if (!permission.granted) {
    return (
      <View style={styles.container}>
        <Button onPress={requestPermission} title="Grant Camera Permission" />
      </View>
    )
  }

  return (
    <View style={styles.container}>
      <Camera style={styles.camera} type={type}>
        <View style={styles.buttonContainer}>
          <Button
            onPress={() => {
              setType(current =>
                current === CameraType.back
                  ? CameraType.front
                  : CameraType.back
              )
            }}
            title="Flip Camera"
          />
        </View>
      </Camera>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  camera: { flex: 1 },
  buttonContainer: {
    flex: 1,
    backgroundColor: 'transparent',
    justifyContent: 'flex-end',
    padding: 20
  }
})

Push Notifications

// hooks/useNotifications.ts
import { useState, useEffect, useRef } from 'react'
import * as Notifications from 'expo-notifications'
import * as Device from 'expo-device'
import { Platform } from 'react-native'

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false
  })
})

export function useNotifications() {
  const [expoPushToken, setExpoPushToken] = useState('')
  const notificationListener = useRef<Notifications.Subscription>()
  const responseListener = useRef<Notifications.Subscription>()

  useEffect(() => {
    registerForPushNotificationsAsync().then(token => setExpoPushToken(token || ''))

    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      console.log('Notification received:', notification)
    })

    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      console.log('Notification clicked:', response)
    })

    return () => {
      Notifications.removeNotificationSubscription(notificationListener.current!)
      Notifications.removeNotificationSubscription(responseListener.current!)
    }
  }, [])

  return { expoPushToken }
}

async function registerForPushNotificationsAsync() {
  let token

  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C'
    })
  }

  if (Device.isDevice) {
    const { status: existingStatus } = await Notifications.getPermissionsAsync()
    let finalStatus = existingStatus

    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync()
      finalStatus = status
    }

    if (finalStatus !== 'granted') {
      alert('Failed to get push token for push notification!')
      return
    }

    token = (await Notifications.getExpoPushTokenAsync()).data
  } else {
    alert('Must use physical device for Push Notifications')
  }

  return token
}

Location

// hooks/useLocation.ts
import { useState, useEffect } from 'react'
import * as Location from 'expo-location'

export function useLocation() {
  const [location, setLocation] = useState<Location.LocationObject | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    ;(async () => {
      const { status } = await Location.requestForegroundPermissionsAsync()

      if (status !== 'granted') {
        setError('Permission to access location was denied')
        return
      }

      const location = await Location.getCurrentPositionAsync({})
      setLocation(location)
    })()
  }, [])

  return { location, error }
}

---

State Management

Zustand (Recommended)

// store/auth.ts
import { create } from 'zustand'

interface User {
  id: string
  email: string
  name: string
}

interface AuthStore {
  user: User | null
  token: string | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
}

export const useAuthStore = create<AuthStore>(set => ({
  user: null,
  token: null,

  login: async (email, password) => {
    const response = await fetch('https://api.example.com/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    const { user, token } = await response.json()

    set({ user, token })
  },

  logout: () => {
    set({ user: null, token: null })
  }
}))

Usage:

// app/login.tsx
import { useState } from 'react'
import { View, TextInput } from 'react-native'
import { useAuthStore } from '@/store/auth'
import { Button } from '@/components/Button'

export default function LoginScreen() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const login = useAuthStore(state => state.login)

  return (
    <View>
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button
        title="Login"
        onPress={() => login(email, password)}
      />
    </View>
  )
}

---

Performance Optimization

Image Optimization

// components/OptimizedImage.tsx
import { Image } from 'expo-image'
import { StyleSheet } from 'react-native'

interface OptimizedImageProps {
  uri: string
  width: number
  height: number
}

export function OptimizedImage({ uri, width, height }: OptimizedImageProps) {
  return (
    <Image
      source={{ uri }}
      style={{ width, height }}
      contentFit="cover"
      transition={200}
      cachePolicy="memory-disk"
      placeholder={require('@/assets/placeholder.png')}
    />
  )
}

Lazy Loading

// app/(tabs)/index.tsx
import { lazy, Suspense } from 'react'
import { View, ActivityIndicator } from 'react-native'

const HeavyComponent = lazy(() => import('@/components/HeavyComponent'))

export default function HomeScreen() {
  return (
    <View>
      <Suspense fallback={<ActivityIndicator />}>
        <HeavyComponent />
      </Suspense>
    </View>
  )
}

List Optimization

import { FlashList } from '@shopify/flash-list'

export default function OptimizedList({ data }) {
  return (
    <FlashList
      data={data}
      renderItem={({ item }) => <Card>{item.title}</Card>}
      estimatedItemSize={100}
      keyExtractor={(item) => item.id}
    />
  )
}

---

App Configuration

app.json

{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "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.myapp",
      "buildNumber": "1"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.yourcompany.myapp",
      "versionCode": 1,
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION", "NOTIFICATIONS"]
    },
    "plugins": [
      "expo-router",
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
        }
      ],
      [
        "expo-location",
        {
          "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
        }
      ]
    ]
  }
}

---

Building and Deploying

Build for iOS

# Install EAS CLI
npm install -g eas-cli

# Login
eas login

# Configure build
eas build:configure

# Build for iOS
eas build --platform ios

# Submit to App Store
eas submit --platform ios

Build for Android

# Build for Android
eas build --platform android

# Submit to Google Play
eas submit --platform android

Over-the-Air (OTA) Updates

# Create update
eas update --branch production --message "Bug fixes"

# Users get update automatically (no app store review)

---

Testing

Jest + React Native Testing Library

// __tests__/Button.test.tsx
import { render, fireEvent } from '@testing-library/react-native'
import { Button } from '@/components/Button'

describe('Button', () => {
  it('calls onPress when pressed', () => {
    const onPress = jest.fn()
    const { getByText } = render(<Button title="Click me" onPress={onPress} />)

    fireEvent.press(getByText('Click me'))

    expect(onPress).toHaveBeenCalledTimes(1)
  })

  it('shows loading indicator when loading', () => {
    const { getByTestId } = render(
      <Button title="Click me" onPress={() => {}} loading />
    )

    expect(getByTestId('loading-indicator')).toBeTruthy()
  })
})

---

Common Patterns

Protected Routes

// app/_layout.tsx
import { useEffect } from 'react'
import { useRouter, Slot } from 'expo-router'
import { useAuthStore } from '@/store/auth'

export default function RootLayout() {
  const router = useRouter()
  const user = useAuthStore(state => state.user)

  useEffect(() => {
    if (!user) {
      router.replace('/login')
    }
  }, [user])

  return <Slot />
}

Form Handling

// hooks/useForm.ts
import { useState } from 'react'

export function useForm<T>(initialValues: T) {
  const [values, setValues] = useState<T>(initialValues)
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})

  const handleChange = (name: keyof T) => (value: any) => {
    setValues(prev => ({ ...prev, [name]: value }))
    setErrors(prev => ({ ...prev, [name]: undefined }))
  }

  const validate = (rules: Partial<Record<keyof T, (value: any) => string | undefined>>) => {
    const newErrors: Partial<Record<keyof T, string>> = {}

    Object.keys(rules).forEach(key => {
      const error = rules[key as keyof T]?.(values[key as keyof T])
      if (error) {
        newErrors[key as keyof T] = error
      }
    })

    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }

  return { values, errors, handleChange, validate }
}

---

When to Use Me

Perfect for:

  • Building iOS + Android apps from one codebase
  • Rapid mobile prototyping
  • Apps with native features (camera, location, push)
  • Cross-platform mobile development

I'll help you:

  • Set up React Native / Expo projects
  • Implement navigation
  • Integrate native features
  • Optimize performance
  • Submit to app stores

What I'll Create

📱 Cross-Platform Apps
🧭 Navigation Systems
📸 Camera Integration
📍 Location Services
🔔 Push Notifications
🚀 App Store Submissions

Let's build amazing mobile experiences!

Related skills

FAQ

What stack does mobile-developer target?

mobile-developer targets React Native with Expo, Expo Router file-based navigation, TypeScript components, Zustand state, and EAS CLI for iOS and Android builds, store submission, and over-the-air updates.

Does mobile-developer cover native device features?

Yes—mobile-developer includes expo-camera, expo-location, and expo-notifications patterns for camera capture, geolocation permissions, and Expo push token registration on physical devices.

How does mobile-developer handle app store releases?

mobile-developer documents eas build for iOS and Android, eas submit to App Store and Google Play, and eas update for OTA production branch updates without full store review cycles.

Mobile Developmentfrontendintegrationstesting

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.