Mobile app development guide for cross-platform apps. Covers React Native, Flutter, and Expo frameworks. Triggers: mobile app, React Native, Flutter, Expo, iOS, Android, 모바일 앱, モバイルアプリ, 移动应用
/plugin marketplace add popup-studio-ai/bkit-claude-code/plugin install bkit@bkit-marketplaceThis skill is limited to using the following tools:
A guide for developing mobile apps based on web development experience. Develop for iOS and Android simultaneously using cross-platform frameworks.
| Framework | Recommended For | Advantages | Disadvantages |
|---|---|---|---|
| Expo (React Native) | Web developers, quick releases | Leverage React knowledge, fast development | Native module limitations |
| React Native CLI | Complex apps, native integration | Full native access | Complex setup |
| Flutter | High-performance UI, willing to learn new | Fast rendering, consistent UI | Need to learn Dart |
Starter → Expo (React Native)
- Simple setup, can leverage web knowledge
Dynamic → Expo + EAS Build
- Includes server integration, production build support
Enterprise → React Native CLI or Flutter
- Complex native features, performance optimization needed
# Install Expo CLI
npm install -g expo-cli
# Create new project
npx create-expo-app my-app
cd my-app
# Start development server
npx expo start
my-app/
├── app/ # Expo Router pages
│ ├── (tabs)/ # Tab navigation
│ │ ├── index.tsx # Home tab
│ │ ├── explore.tsx # Explore tab
│ │ └── _layout.tsx # Tab layout
│ ├── _layout.tsx # Root layout
│ └── +not-found.tsx # 404 page
├── components/ # Reusable components
├── hooks/ # Custom hooks
├── constants/ # Constants
├── assets/ # Images, fonts, etc.
├── app.json # Expo configuration
└── package.json
// app/_layout.tsx - Stack navigation
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
);
}
// app/(tabs)/_layout.tsx - Tab navigation
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Ionicons name="home" color={color} size={24} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color }) => <Ionicons name="person" color={color} size={24} />,
}}
/>
</Tabs>
);
}
// Basic StyleSheet
import { StyleSheet, View, Text } from 'react-native';
export function MyComponent() {
return (
<View style={styles.container}>
<Text style={styles.title}>Hello</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
// NativeWind (Tailwind for RN) - Recommended
import { View, Text } from 'react-native';
export function MyComponent() {
return (
<View className="flex-1 p-4 bg-white">
<Text className="text-2xl font-bold">Hello</Text>
</View>
);
}
// hooks/useApi.ts
import { useState, useEffect } from 'react';
export function useApi<T>(endpoint: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}${endpoint}`);
if (!response.ok) throw new Error('API Error');
const json = await response.json();
setData(json);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, [endpoint]);
return { data, loading, error };
}
// context/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
interface AuthContextType {
user: User | null;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Check for stored token on app start
const loadToken = async () => {
const token = await SecureStore.getItemAsync('authToken');
if (token) {
// Load user info with token
}
};
loadToken();
}, []);
const signIn = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const { token, user } = await response.json();
await SecureStore.setItemAsync('authToken', token);
setUser(user);
};
const signOut = async () => {
await SecureStore.deleteItemAsync('authToken');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext)!;
# After installing Flutter SDK
flutter create my_app
cd my_app
# Start development server
flutter run
my_app/
├── lib/
│ ├── main.dart # App entry point
│ ├── app/
│ │ ├── app.dart # MaterialApp setup
│ │ └── routes.dart # Route definitions
│ ├── features/ # Feature-based folders
│ │ ├── auth/
│ │ │ ├── screens/
│ │ │ ├── widgets/
│ │ │ └── providers/
│ │ └── home/
│ ├── shared/
│ │ ├── widgets/ # Common widgets
│ │ ├── services/ # API services
│ │ └── models/ # Data models
│ └── core/
│ ├── theme/ # Theme settings
│ └── constants/ # Constants
├── assets/ # Images, fonts
├── pubspec.yaml # Dependency management
└── android/ & ios/ # Native code
// lib/features/home/screens/home_screen.dart
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: const Center(
child: Text('Hello, Flutter!'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
// lib/providers/counter_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// Usage
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
| Element | Web | Mobile |
|---|---|---|
| Click | onClick | onPress |
| Scroll | overflow: scroll | ScrollView / FlatList |
| Input | input | TextInput |
| Links | a href | Link / navigation |
| Layout | div + CSS | View + StyleSheet |
Web: URL-based (browser back button)
Mobile: Stack-based (screen stacking)
Web: /users/123
Mobile: navigation.navigate('User', { id: 123 })
Web: localStorage, sessionStorage, Cookie
Mobile: AsyncStorage, SecureStore, SQLite
⚠️ SecureStore is required for sensitive info on mobile!
# Install EAS CLI
npm install -g eas-cli
# Login
eas login
# Configure build
eas build:configure
# iOS build
eas build --platform ios
# Android build
eas build --platform android
# Submit to stores
eas submit --platform ios
eas submit --platform android
// app.json
{
"expo": {
"extra": {
"apiUrl": "https://api.example.com"
}
}
}
// Usage
import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig?.extra?.apiUrl;
□ Identify data that needs offline caching
□ Define sync conflict resolution strategy
□ Follow iOS/Android native UX guidelines
□ Consider gestures (swipe, pinch, etc.)
□ Layout for different screen sizes (phone, tablet)
□ Keyboard handling (screen adjustment during input)
□ Safe Area handling (notch, home button area)
□ Handle platform-specific UI differences
□ Store sensitive info with SecureStore
□ Certificate Pinning (if needed)
□ App obfuscation settings
□ Follow App Store review guidelines
□ Prepare Privacy Policy URL
□ Prepare screenshots, app description
A: Recommend separate project over full conversion
- APIs can be shared
- UI needs to be rewritten (for native UX)
- Business logic can be shared
A: Start with Expo!
- 90%+ of apps are sufficient with Expo
- Can eject later if needed
- Use CLI only when native modules are absolutely required
A:
- iOS: 1-7 days (average 2-3 days)
- Android: Few hours ~ 3 days
⚠️ First submission has high rejection possibility → Follow guidelines carefully!
"Set up a [app description] app project with React Native + Expo.
Configure with 3 tab navigation (Home, Search, Profile)."
"Implement [screen name] screen.
- Display [content] at the top
- Display [list/form/etc.] in the middle
- [Button/navigation] at the bottom"
"Implement screen integrating with [API endpoint].
- Show loading state
- Handle errors
- Support pull-to-refresh"
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.