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 (
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 (
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 (
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 (
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
export function useQuery
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
if (error) {
return (
return (
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
if (!permission.granted) {
return (
return (
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
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
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
export const useAuthStore = create
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 (
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 (
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 (
List Optimization import { FlashList } from '@shopify/flash-list'
export default function OptimizedList({ data }) {
return (
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()
fireEvent.press(getByText('Click me'))
expect(onPress).toHaveBeenCalledTimes(1)
})
it('shows loading indicator when loading', () => { const { getByTestId } = render(
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
Form Handling // hooks/useForm.ts import { useState } from 'react'
export function useForm
const handleChange = (name: keyof T) => (value: any) => { setValues(prev => ({ ...prev, [name]: value })) setErrors(prev => ({ ...prev, [name]: undefined })) }
const validate = (rules: Partial
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!