MCPInstall

mobile-developer

Expert in React Native, Expo, and cross-platform mobile development

Installs
412
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/daffy0208/ai-dev-standards --skill mobile-developer
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads mobile-developer automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

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!

FAQ

What does the mobile-developer skill do?

Expert in React Native, Expo, and cross-platform mobile development

How do I install mobile-developer?

Run npx skills add https://github.com/daffy0208/ai-dev-standards --skill mobile-developer in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/mobile-developer/ (personal) or .claude/skills/mobile-developer/ (project).

Which AI agents can use mobile-developer?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is mobile-developer safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains mobile-developer?

It is published by daffy0208 in daffy0208/ai-dev-standards. It has 412 recorded installs.

More from daffy0208/ai-dev-standards

View all →

animation-designer

daffy0208/ai-dev-standards

Expert in web animations, transitions, and motion design using Framer Motion and CSS

Frontend & Design1.9K installs

brand-designer

daffy0208/ai-dev-standards

Expert in brand identity, logo design, and visual brand systems

Marketing & SEO1.3K installs

3d-visualizer

daffy0208/ai-dev-standards

Expert in Three.js, 3D graphics, and interactive 3D visualizations

Media & Creative665 installs

data-visualizer

daffy0208/ai-dev-standards

Expert in creating charts, dashboards, and data visualizations using modern libraries

572 installs

video-producer

daffy0208/ai-dev-standards

Open the skill to read its SKILL.md instructions.

Media & Creative449 installs

knowledge base manager

daffy0208/ai-dev-standards

Open the skill to read its SKILL.md instructions.

438 installs

Popular mobile skills

View category →

design-mobile-apps

designed-by-ai/skills

Design mobile app screens with Sleek, edit Sleek projects, and implement their designs in React Native or HTML.

Mobile1.1M installs

ios-design

uizze.sh

Design and review iOS UI/UX for iPhone apps in SwiftUI, UIKit, or React Native. Use for mobile app design, screen redesigns, navigation flows, typography, forms, keyboard and safe-area layout, and accessibility checks. Apply Apple Human Interface Guidelines and consult real product references through the optional Uizze connection.

Mobile416.5K installs

vercel-react-native-skills

vercel-labs/agent-skills

React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.

OfficialMobile256.7K installs

animate-expo

emilkowalski/skills

Build animations in React Native and Expo, making the decisions in the order that determines whether they feel right — should it animate, which thread it runs on, which properties, spring or timing, how the gesture hands off, how it degrades. Writes the implementation with Reanimated, Gesture Handler, Expo Router and expo-haptics. Use when animating anything in an Expo app, adding gestures, sheets, screen transitions, press feedback or haptics, or fixing motion that stutters on device. For web animation use animate.

Mobile140.1K installs

xcode-project-setup

firebase/agent-skills

Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).

OfficialMobile131.2K installs

firebase-remote-config-basics

firebase/agent-skills

Manages Firebase Remote Config templates, feature flags, loading strategies, and SDKs (Android, iOS). Use when downloading/deploying remoteconfig JSON templates, managing version history/feature flags, setting in-app defaults, fetchAndActivate(), real-time listeners, or SDK setup. Don't use for Firebase Hosting, Auth, Firestore, Data Connect, Crashlytics, or App Hosting.

OfficialMobile121.7K installs