بناء تطبيقات الموبايل مع AI
إعداد تطبيق الموبايل مع Expo
Project Goal: Build a cross-platform mobile app that works on iOS, Android, and web using Expo SDK 52 with AI assistance.
Why Expo for Vibe Coding?
Expo provides the fastest path from idea to mobile app. With Expo SDK 52+ and the new architecture:
- Zero native setup - No Xcode/Android Studio initially required
- Hot reloading - See changes instantly on your device
- AI-friendly - Single JavaScript/TypeScript codebase is easier for AI to understand
- Web support - Same code runs in browsers
- EAS Build - Cloud builds without local native toolchains
Project Initialization Prompt
Create a new Expo SDK 52 mobile app with:
## Tech Stack
- Expo SDK 52 with New Architecture enabled
- Expo Router v4 for file-based navigation
- NativeWind v4 for Tailwind CSS styling
- Zustand for state management
- React Query for server state
- TypeScript strict mode
## Project Structure
/app # Expo Router screens /(tabs) # Tab navigation group /index.tsx # Home tab /explore.tsx # Explore tab /profile.tsx # Profile tab /(auth) # Auth screens (no tabs) /login.tsx /register.tsx /_layout.tsx # Root layout /components # Shared components /ui # Base UI components /lib # Utilities and helpers /hooks # Custom hooks /stores # Zustand stores /services # API services /constants # App constants and theme
## Initial Setup
1. Initialize with: npx create-expo-app@latest
2. Enable New Architecture in app.json
3. Configure NativeWind with CSS variables for theming
4. Set up path aliases (@/ for root)
5. Create base UI components: Button, Input, Card, Text
Understanding Expo Router v4
Expo Router uses file-based routing similar to Next.js:
// app/_layout.tsx - Root layout
import { Stack } from 'expo-router';
import { ThemeProvider } from '@/components/theme-provider';
import '../global.css';
export default function RootLayout() {
return (
<ThemeProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="(auth)"
options={{ presentation: 'modal' }}
/>
</Stack>
</ThemeProvider>
);
}
// app/(tabs)/_layout.tsx - Tab navigation
import { Tabs } from 'expo-router';
import { Home, Search, User } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#6366f1',
headerShown: true,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Home size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color, size }) => (
<Search size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<User size={size} color={color} />
),
}}
/>
</Tabs>
);
}
NativeWind v4 Configuration
NativeWind brings Tailwind CSS to React Native:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
primary: {
DEFAULT: 'var(--color-primary)',
foreground: 'var(--color-primary-foreground)',
},
background: 'var(--color-background)',
foreground: 'var(--color-foreground)',
muted: {
DEFAULT: 'var(--color-muted)',
foreground: 'var(--color-muted-foreground)',
},
card: {
DEFAULT: 'var(--color-card)',
foreground: 'var(--color-card-foreground)',
},
},
},
},
plugins: [],
};
/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-primary: #6366f1;
--color-primary-foreground: #ffffff;
--color-background: #ffffff;
--color-foreground: #09090b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-card: #ffffff;
--color-card-foreground: #09090b;
}
.dark {
--color-primary: #818cf8;
--color-primary-foreground: #09090b;
--color-background: #09090b;
--color-foreground: #fafafa;
--color-muted: #27272a;
--color-muted-foreground: #a1a1aa;
--color-card: #18181b;
--color-card-foreground: #fafafa;
}
Building Base UI Components
Create reusable components that work consistently across platforms:
// components/ui/button.tsx
import { Pressable, Text, View, ActivityIndicator } from 'react-native';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'flex-row items-center justify-center rounded-lg',
{
variants: {
variant: {
default: 'bg-primary',
secondary: 'bg-muted',
outline: 'border border-primary bg-transparent',
ghost: 'bg-transparent',
destructive: 'bg-red-500',
},
size: {
default: 'h-12 px-6',
sm: 'h-9 px-4',
lg: 'h-14 px-8',
icon: 'h-12 w-12',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const textVariants = cva('font-semibold', {
variants: {
variant: {
default: 'text-primary-foreground',
secondary: 'text-foreground',
outline: 'text-primary',
ghost: 'text-foreground',
destructive: 'text-white',
},
size: {
default: 'text-base',
sm: 'text-sm',
lg: 'text-lg',
icon: 'text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});
interface ButtonProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
onPress?: () => void;
disabled?: boolean;
loading?: boolean;
className?: string;
}
export function Button({
children,
variant,
size,
onPress,
disabled,
loading,
className,
}: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled || loading}
className={cn(
buttonVariants({ variant, size }),
disabled && 'opacity-50',
className
)}
>
{loading ? (
<ActivityIndicator
color={variant === 'default' ? '#fff' : '#6366f1'}
/>
) : (
<Text className={cn(textVariants({ variant, size }))}>
{children}
</Text>
)}
</Pressable>
);
}
// components/ui/input.tsx
import { TextInput, View, Text } from 'react-native';
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends React.ComponentProps<typeof TextInput> {
label?: string;
error?: string;
containerClassName?: string;
}
export const Input = forwardRef<TextInput, InputProps>(
({ label, error, containerClassName, className, ...props }, ref) => {
return (
<View className={cn('gap-1.5', containerClassName)}>
{label && (
<Text className="text-sm font-medium text-foreground">
{label}
</Text>
)}
<TextInput
ref={ref}
className={cn(
'h-12 rounded-lg border border-muted bg-background px-4',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary',
error && 'border-red-500',
className
)}
placeholderTextColor="#71717a"
{...props}
/>
{error && (
<Text className="text-sm text-red-500">{error}</Text>
)}
</View>
);
}
);
State Management with Zustand
// stores/auth-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
setUser: (user: User | null) => void;
setToken: (token: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isLoading: false,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
API Layer with React Query
// lib/api-client.ts
import { useAuthStore } from '@/stores/auth-store';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
class ApiClient {
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = useAuthStore.getState().token;
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || 'Request failed');
}
return response.json();
}
get<T>(endpoint: string) {
return this.request<T>(endpoint);
}
post<T>(endpoint: string, data: unknown) {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
put<T>(endpoint: string, data: unknown) {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
delete<T>(endpoint: string) {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
export const api = new ApiClient();
// services/posts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
interface Post {
id: string;
title: string;
content: string;
createdAt: string;
}
export function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: () => api.get<Post[]>('/posts'),
});
}
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { title: string; content: string }) =>
api.post<Post>('/posts', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
App Configuration
// app.json
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#6366f1"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#6366f1"
},
"package": "com.yourcompany.myapp"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"expo-secure-store"
],
"experiments": {
"typedRoutes": true
}
}
}
AI Prompt: Debugging Mobile Issues
When you encounter mobile-specific issues, use this prompt template:
I'm getting this error in my Expo app:
[Paste the error message]
## Environment
- Expo SDK: 52
- Platform: iOS/Android/Web
- Device: Physical device / Simulator
- Running via: Expo Go / Development build
## Relevant code
[Paste the component or configuration causing issues]
## What I've tried
1. [List attempted solutions]
Please help me understand and fix this issue.
Testing on Devices
# Start development server
npx expo start
# Run on iOS simulator
npx expo run:ios
# Run on Android emulator
npx expo run:android
# Create development build for physical devices
eas build --profile development --platform ios
eas build --profile development --platform android
Key Takeaways
- Expo Router provides file-based navigation similar to Next.js
- NativeWind v4 enables Tailwind CSS with CSS variables for theming
- Zustand + React Query handle local and server state efficiently
- Component variants (using CVA) create consistent, reusable UI
- Development builds are needed for native modules not in Expo Go
إعداد تطبيق الموبايل مع Expo
هدف المشروع: بناء تطبيق موبايل متعدد المنصات يعمل على iOS وAndroid والويب باستخدام Expo SDK 52 بمساعدة AI.
لماذا Expo للـ Vibe Coding؟
Expo يوفر أسرع طريق من الفكرة إلى تطبيق الموبايل. مع Expo SDK 52+ والهندسة الجديدة:
- صفر إعداد native - لا حاجة لـ Xcode/Android Studio في البداية
- إعادة التحميل الفوري - شاهد التغييرات فوراً على جهازك
- صديق للـ AI - قاعدة كود JavaScript/TypeScript واحدة أسهل للـ AI لفهمها
- دعم الويب - نفس الكود يعمل في المتصفحات
- EAS Build - بناء سحابي بدون أدوات native محلية
بروم�ت تهيئة المشروع
أنشئ تطبيق Expo SDK 52 موبايل جديد مع:
## التقنيات المستخدمة
- Expo SDK 52 مع تفعيل الهندسة الجديدة
- Expo Router v4 للتنقل المبني على الملفات
- NativeWind v4 لتنسيقات Tailwind CSS
- Zustand لإدارة الحالة
- React Query لحالة الخادم
- TypeScript وضع صارم
## هيكل المشروع
/app # شاشات Expo Router /(tabs) # مجموعة التنقل بالتابات /index.tsx # تاب الرئيسية /explore.tsx # تاب الاستكشاف /profile.tsx # تاب الملف الشخصي /(auth) # شاشات المصادقة (بدون تابات) /login.tsx /register.tsx /_layout.tsx # التخطيط الجذري /components # المكونات المشتركة /ui # مكونات UI الأساسية /lib # الأدوات المساعدة /hooks # الهوكس المخصصة /stores # مخازن Zustand /services # خدمات API /constants # الثوابت والثيم
## الإعداد الأولي
1. التهيئة بـ: npx create-expo-app@latest
2. تفعيل الهندسة الجديدة في app.json
3. تكوين NativeWind مع متغيرات CSS للثيمات
4. إعداد اختصارات المسارات (@/ للجذر)
5. إنشاء مكونات UI الأساسية: Button, Input, Card, Text
فهم Expo Router v4
Expo Router يستخدم التوجيه المبني على الملفات مشابهاً لـ Next.js:
// app/_layout.tsx - التخطيط الجذري
import { Stack } from 'expo-router';
import { ThemeProvider } from '@/components/theme-provider';
import '../global.css';
export default function RootLayout() {
return (
<ThemeProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="(auth)"
options={{ presentation: 'modal' }}
/>
</Stack>
</ThemeProvider>
);
}
// app/(tabs)/_layout.tsx - التنقل بالتابات
import { Tabs } from 'expo-router';
import { Home, Search, User } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#6366f1',
headerShown: true,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'الرئيسية',
tabBarIcon: ({ color, size }) => (
<Home size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'استكشاف',
tabBarIcon: ({ color, size }) => (
<Search size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'الملف الشخصي',
tabBarIcon: ({ color, size }) => (
<User size={size} color={color} />
),
}}
/>
</Tabs>
);
}
تكوين NativeWind v4
NativeWind يجلب Tailwind CSS إلى React Native:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
primary: {
DEFAULT: 'var(--color-primary)',
foreground: 'var(--color-primary-foreground)',
},
background: 'var(--color-background)',
foreground: 'var(--color-foreground)',
muted: {
DEFAULT: 'var(--color-muted)',
foreground: 'var(--color-muted-foreground)',
},
card: {
DEFAULT: 'var(--color-card)',
foreground: 'var(--color-card-foreground)',
},
},
},
},
plugins: [],
};
/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-primary: #6366f1;
--color-primary-foreground: #ffffff;
--color-background: #ffffff;
--color-foreground: #09090b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-card: #ffffff;
--color-card-foreground: #09090b;
}
.dark {
--color-primary: #818cf8;
--color-primary-foreground: #09090b;
--color-background: #09090b;
--color-foreground: #fafafa;
--color-muted: #27272a;
--color-muted-foreground: #a1a1aa;
--color-card: #18181b;
--color-card-foreground: #fafafa;
}
بناء مكونات UI الأساسية
إنشاء مكونات قابلة لإعادة الاستخدام تعمل بشكل متسق عبر المنصات:
// components/ui/button.tsx
import { Pressable, Text, View, ActivityIndicator } from 'react-native';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'flex-row items-center justify-center rounded-lg',
{
variants: {
variant: {
default: 'bg-primary',
secondary: 'bg-muted',
outline: 'border border-primary bg-transparent',
ghost: 'bg-transparent',
destructive: 'bg-red-500',
},
size: {
default: 'h-12 px-6',
sm: 'h-9 px-4',
lg: 'h-14 px-8',
icon: 'h-12 w-12',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const textVariants = cva('font-semibold', {
variants: {
variant: {
default: 'text-primary-foreground',
secondary: 'text-foreground',
outline: 'text-primary',
ghost: 'text-foreground',
destructive: 'text-white',
},
size: {
default: 'text-base',
sm: 'text-sm',
lg: 'text-lg',
icon: 'text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});
interface ButtonProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
onPress?: () => void;
disabled?: boolean;
loading?: boolean;
className?: string;
}
export function Button({
children,
variant,
size,
onPress,
disabled,
loading,
className,
}: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled || loading}
className={cn(
buttonVariants({ variant, size }),
disabled && 'opacity-50',
className
)}
>
{loading ? (
<ActivityIndicator
color={variant === 'default' ? '#fff' : '#6366f1'}
/>
) : (
<Text className={cn(textVariants({ variant, size }))}>
{children}
</Text>
)}
</Pressable>
);
}
// components/ui/input.tsx
import { TextInput, View, Text } from 'react-native';
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends React.ComponentProps<typeof TextInput> {
label?: string;
error?: string;
containerClassName?: string;
}
export const Input = forwardRef<TextInput, InputProps>(
({ label, error, containerClassName, className, ...props }, ref) => {
return (
<View className={cn('gap-1.5', containerClassName)}>
{label && (
<Text className="text-sm font-medium text-foreground">
{label}
</Text>
)}
<TextInput
ref={ref}
className={cn(
'h-12 rounded-lg border border-muted bg-background px-4',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary',
error && 'border-red-500',
className
)}
placeholderTextColor="#71717a"
{...props}
/>
{error && (
<Text className="text-sm text-red-500">{error}</Text>
)}
</View>
);
}
);
إدارة الحالة مع Zustand
// stores/auth-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
setUser: (user: User | null) => void;
setToken: (token: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isLoading: false,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
طبقة API مع React Query
// lib/api-client.ts
import { useAuthStore } from '@/stores/auth-store';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
class ApiClient {
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = useAuthStore.getState().token;
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || 'Request failed');
}
return response.json();
}
get<T>(endpoint: string) {
return this.request<T>(endpoint);
}
post<T>(endpoint: string, data: unknown) {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
put<T>(endpoint: string, data: unknown) {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
delete<T>(endpoint: string) {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
export const api = new ApiClient();
// services/posts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
interface Post {
id: string;
title: string;
content: string;
createdAt: string;
}
export function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: () => api.get<Post[]>('/posts'),
});
}
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { title: string; content: string }) =>
api.post<Post>('/posts', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
تكوين التطبيق
// app.json
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#6366f1"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#6366f1"
},
"package": "com.yourcompany.myapp"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"expo-secure-store"
],
"experiments": {
"typedRoutes": true
}
}
}
برومبت AI: تصحيح مشاكل الموبايل
عندما تواجه مشاكل خاصة بالموبايل، استخدم قالب البرومبت هذا:
أحصل على هذا الخطأ في تطبيق Expo:
[الصق رسالة الخطأ]
## البيئة
- Expo SDK: 52
- المنصة: iOS/Android/Web
- الجهاز: جهاز فعلي / محاكي
- التشغيل عبر: Expo Go / Development build
## الكود ذو الصلة
[الصق المكون أو التكوين المسبب للمشكلة]
## ما جربته
1. [اذكر الحلول المجربة]
من فضلك ساعدني في فهم وإصلاح هذه المشكلة.
الاختبار على الأجهزة
# بدء خادم التطوير
npx expo start
# التشغيل على محاكي iOS
npx expo run:ios
# التشغيل على محاكي Android
npx expo run:android
# إنشاء development build للأجهزة الفعلية
eas build --profile development --platform ios
eas build --profile development --platform android
النقاط الرئيسية
- Expo Router يوفر تنقلاً مبنياً على الملفات مشابهاً لـ Next.js
- NativeWind v4 يُمكّن Tailwind CSS مع متغيرات CSS للثيمات
- Zustand + React Query يتعاملان مع الحالة المحلية وحالة الخادم بكفاءة
- متغيرات المكونات (باستخدام CVA) تنشئ UI متسق وقابل لإعادة الاستخدام
- Development builds مطلوبة للوحدات الأصلية غير الموجودة في Expo Go