بناء تطبيق موبايل متعدد المنصات باستخدام Expo و React Native و TypeScript في 2026

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

كود واحد، منصتان. أصبح Expo الطريقة الافتراضية لبناء تطبيقات React Native في 2026. مع التوجيه القائم على الملفات، ودعم الوحدات الأصلية، والتحديثات عبر الهواء، يزيل تعقيد تطوير الموبايل مع الحفاظ على كامل القدرات الأصلية. في هذا الدليل، ستبني تطبيقاً كاملاً لإدارة المهام يعمل على iOS و Android.

ما ستبنيه

تطبيق TaskFlow كامل المزايا — تطبيق إدارة مهام يتضمن:

  • تنقل قائم على الملفات باستخدام Expo Router v4
  • واجهة جميلة مع NativeWind (Tailwind CSS لـ React Native)
  • تخزين بيانات محلي باستخدام Expo SQLite
  • دمج الكاميرا لإرفاق صور بالمهام
  • إشعارات فورية لتذكيرات المهام
  • دعم الوضع الداكن
  • إعدادات جاهزة للنشر

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبّت (node --version)
  • هاتف ذكي (iOS أو Android) مع تطبيق Expo Go مثبّت
  • معرفة أساسية بـ React و TypeScript
  • VS Code مع إضافة Expo Tools (موصى به)
  • حساب Expo (مجاني على expo.dev)

الخطوة 1: إنشاء مشروع Expo

ابدأ بإنشاء مشروع Expo جديد مع أحدث SDK:

npx create-expo-app@latest TaskFlow --template tabs
cd TaskFlow

هذا ينشئ مشروعاً مع التوجيه القائم على الملفات مُعدّ مسبقاً. لنفحص الهيكل:

TaskFlow/
├── app/                    # المسارات القائمة على الملفات
│   ├── (tabs)/             # مجموعة تبويبات التنقل
│   │   ├── _layout.tsx     # إعدادات التبويبات
│   │   ├── index.tsx       # تبويب الرئيسية
│   │   └── explore.tsx     # تبويب الاستكشاف
│   ├── _layout.tsx         # التخطيط الجذري
│   └── +not-found.tsx      # صفحة 404
├── assets/                 # الصور والخطوط
├── components/             # المكونات القابلة لإعادة الاستخدام
├── constants/              # الثوابت والإعدادات
└── package.json

ثبّت التبعيات الإضافية التي سنحتاجها:

npx expo install expo-sqlite expo-camera expo-notifications expo-image-picker nativewind tailwindcss

الخطوة 2: إعداد NativeWind (Tailwind CSS)

NativeWind يجلب فئات Tailwind CSS إلى React Native. قم بإعداده:

npx expo install nativewind tailwindcss react-native-reanimated

أنشئ ملف 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: "#6366f1",
        secondary: "#8b5cf6",
      },
    },
  },
  plugins: [],
};

أنشئ ملف global.css في جذر المشروع:

@tailwind base;
@tailwind components;
@tailwind utilities;

حدّث babel.config.js ليشمل إضافة NativeWind:

module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};

الآن حدّث app/_layout.tsx لاستيراد الأنماط العامة:

import "../global.css";
import { Stack } from "expo-router";
import { useColorScheme } from "react-native";
import {
  DarkTheme,
  DefaultTheme,
  ThemeProvider,
} from "@react-navigation/native";
 
export default function RootLayout() {
  const colorScheme = useColorScheme();
 
  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="+not-found" />
      </Stack>
    </ThemeProvider>
  );
}

الخطوة 3: إعداد قاعدة البيانات مع Expo SQLite

أنشئ ملف lib/database.ts لتخزين البيانات محلياً:

import * as SQLite from "expo-sqlite";
 
const db = SQLite.openDatabaseSync("taskflow.db");
 
export interface Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  priority: "low" | "medium" | "high";
  imageUri?: string;
  createdAt: string;
  dueDate?: string;
}
 
export function initDatabase(): void {
  db.execSync(`
    CREATE TABLE IF NOT EXISTS tasks (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      description TEXT DEFAULT '',
      completed INTEGER DEFAULT 0,
      priority TEXT DEFAULT 'medium',
      imageUri TEXT,
      createdAt TEXT DEFAULT (datetime('now')),
      dueDate TEXT
    );
  `);
}
 
export function getTasks(): Task[] {
  const results = db.getAllSync(
    "SELECT * FROM tasks ORDER BY completed ASC, createdAt DESC"
  );
  return results.map((row: any) => ({
    ...row,
    completed: Boolean(row.completed),
  }));
}
 
export function addTask(
  title: string,
  description: string,
  priority: string,
  dueDate?: string
): void {
  db.runSync(
    "INSERT INTO tasks (title, description, priority, dueDate) VALUES (?, ?, ?, ?)",
    [title, description, priority, dueDate ?? null]
  );
}
 
export function toggleTask(id: number, completed: boolean): void {
  db.runSync("UPDATE tasks SET completed = ? WHERE id = ?", [
    completed ? 1 : 0,
    id,
  ]);
}
 
export function deleteTask(id: number): void {
  db.runSync("DELETE FROM tasks WHERE id = ?", [id]);
}
 
export function updateTaskImage(id: number, imageUri: string): void {
  db.runSync("UPDATE tasks SET imageUri = ? WHERE id = ?", [imageUri, id]);
}

هيّئ قاعدة البيانات في app/_layout.tsx:

import { useEffect } from "react";
import { initDatabase } from "@/lib/database";
 
export default function RootLayout() {
  const colorScheme = useColorScheme();
 
  useEffect(() => {
    initDatabase();
  }, []);
 
  // ... بقية التخطيط
}

الخطوة 4: بناء نظام التبويبات

حدّث app/(tabs)/_layout.tsx لتبويبات تطبيق المهام:

import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { useColorScheme } from "react-native";
 
export default function TabLayout() {
  const colorScheme = useColorScheme();
 
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#6366f1",
        tabBarStyle: {
          backgroundColor: colorScheme === "dark" ? "#1a1a2e" : "#ffffff",
        },
        headerStyle: {
          backgroundColor: colorScheme === "dark" ? "#1a1a2e" : "#ffffff",
        },
        headerTintColor: colorScheme === "dark" ? "#ffffff" : "#000000",
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "المهام",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="list" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="add"
        options={{
          title: "إضافة مهمة",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="add-circle" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="stats"
        options={{
          title: "الإحصائيات",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="stats-chart" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

الخطوة 5: إنشاء شاشة قائمة المهام

استبدل محتوى app/(tabs)/index.tsx:

import { View, Text, FlatList, Pressable, RefreshControl } from "react-native";
import { useState, useCallback } from "react";
import { useFocusEffect } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { getTasks, toggleTask, deleteTask, Task } from "@/lib/database";
 
function PriorityBadge({ priority }: { priority: string }) {
  const colors = {
    high: "bg-red-100 text-red-700",
    medium: "bg-yellow-100 text-yellow-700",
    low: "bg-green-100 text-green-700",
  };
 
  const labels = {
    high: "عالية",
    medium: "متوسطة",
    low: "منخفضة",
  };
 
  return (
    <View className={`px-2 py-1 rounded-full ${colors[priority as keyof typeof colors]}`}>
      <Text className={`text-xs font-medium ${colors[priority as keyof typeof colors]}`}>
        {labels[priority as keyof typeof labels]}
      </Text>
    </View>
  );
}
 
function TaskItem({
  task,
  onToggle,
  onDelete,
}: {
  task: Task;
  onToggle: () => void;
  onDelete: () => void;
}) {
  return (
    <View className="bg-white dark:bg-gray-800 mx-4 mb-3 p-4 rounded-2xl shadow-sm flex-row items-center">
      <Pressable onPress={onToggle} className="mr-3">
        <Ionicons
          name={task.completed ? "checkmark-circle" : "ellipse-outline"}
          size={28}
          color={task.completed ? "#22c55e" : "#9ca3af"}
        />
      </Pressable>
 
      <View className="flex-1">
        <Text
          className={`text-base font-semibold dark:text-white ${
            task.completed ? "line-through text-gray-400" : ""
          }`}
        >
          {task.title}
        </Text>
        {task.description ? (
          <Text className="text-sm text-gray-500 mt-1">{task.description}</Text>
        ) : null}
        <View className="flex-row items-center mt-2 gap-2">
          <PriorityBadge priority={task.priority} />
          {task.dueDate && (
            <Text className="text-xs text-gray-400">
              الموعد: {new Date(task.dueDate).toLocaleDateString("ar")}
            </Text>
          )}
        </View>
      </View>
 
      <Pressable onPress={onDelete} className="ml-2 p-2">
        <Ionicons name="trash-outline" size={20} color="#ef4444" />
      </Pressable>
    </View>
  );
}
 
export default function TasksScreen() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [refreshing, setRefreshing] = useState(false);
 
  const loadTasks = useCallback(() => {
    const data = getTasks();
    setTasks(data);
  }, []);
 
  useFocusEffect(
    useCallback(() => {
      loadTasks();
    }, [loadTasks])
  );
 
  const onRefresh = useCallback(() => {
    setRefreshing(true);
    loadTasks();
    setRefreshing(false);
  }, [loadTasks]);
 
  const handleToggle = (id: number, completed: boolean) => {
    toggleTask(id, !completed);
    loadTasks();
  };
 
  const handleDelete = (id: number) => {
    deleteTask(id);
    loadTasks();
  };
 
  const completedCount = tasks.filter((t) => t.completed).length;
 
  return (
    <View className="flex-1 bg-gray-50 dark:bg-gray-900">
      <View className="px-4 pt-4 pb-2">
        <Text className="text-2xl font-bold dark:text-white">مهامي</Text>
        <Text className="text-sm text-gray-500 mt-1">
          {completedCount}/{tasks.length} مكتملة
        </Text>
      </View>
 
      <FlatList
        data={tasks}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <TaskItem
            task={item}
            onToggle={() => handleToggle(item.id, item.completed)}
            onDelete={() => handleDelete(item.id)}
          />
        )}
        refreshControl={
          <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
        }
        contentContainerStyle={{ paddingTop: 8, paddingBottom: 100 }}
        ListEmptyComponent={
          <View className="items-center justify-center pt-20">
            <Ionicons name="clipboard-outline" size={64} color="#d1d5db" />
            <Text className="text-gray-400 text-lg mt-4">لا توجد مهام بعد</Text>
            <Text className="text-gray-400 text-sm mt-1">
              اضغط على تبويب + لإضافة أول مهمة
            </Text>
          </View>
        }
      />
    </View>
  );
}

الخطوة 6: إنشاء شاشة إضافة مهمة

أنشئ ملف app/(tabs)/add.tsx:

import { View, Text, TextInput, Pressable, ScrollView, Alert } from "react-native";
import { useState } from "react";
import { router } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { addTask } from "@/lib/database";
 
const priorities = [
  { value: "low", label: "منخفضة", color: "bg-green-500", icon: "arrow-down" },
  { value: "medium", label: "متوسطة", color: "bg-yellow-500", icon: "remove" },
  { value: "high", label: "عالية", color: "bg-red-500", icon: "arrow-up" },
] as const;
 
export default function AddTaskScreen() {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [priority, setPriority] = useState<string>("medium");
 
  const handleSubmit = () => {
    if (!title.trim()) {
      Alert.alert("خطأ", "عنوان المهمة مطلوب");
      return;
    }
 
    addTask(title.trim(), description.trim(), priority);
    Alert.alert("نجاح", "تم إنشاء المهمة!", [
      { text: "حسناً", onPress: () => router.navigate("/(tabs)") },
    ]);
    setTitle("");
    setDescription("");
    setPriority("medium");
  };
 
  return (
    <ScrollView className="flex-1 bg-gray-50 dark:bg-gray-900">
      <View className="p-4">
        <Text className="text-2xl font-bold mb-6 dark:text-white">
          مهمة جديدة
        </Text>
 
        {/* حقل العنوان */}
        <View className="mb-4">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            العنوان *
          </Text>
          <TextInput
            value={title}
            onChangeText={setTitle}
            placeholder="ما الذي يجب إنجازه؟"
            placeholderTextColor="#9ca3af"
            className="bg-white dark:bg-gray-800 p-4 rounded-xl text-base dark:text-white border border-gray-200 dark:border-gray-700"
          />
        </View>
 
        {/* حقل الوصف */}
        <View className="mb-4">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            الوصف
          </Text>
          <TextInput
            value={description}
            onChangeText={setDescription}
            placeholder="أضف تفاصيل..."
            placeholderTextColor="#9ca3af"
            multiline
            numberOfLines={4}
            textAlignVertical="top"
            className="bg-white dark:bg-gray-800 p-4 rounded-xl text-base dark:text-white border border-gray-200 dark:border-gray-700 min-h-[120px]"
          />
        </View>
 
        {/* اختيار الأولوية */}
        <View className="mb-6">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            الأولوية
          </Text>
          <View className="flex-row gap-3">
            {priorities.map((p) => (
              <Pressable
                key={p.value}
                onPress={() => setPriority(p.value)}
                className={`flex-1 flex-row items-center justify-center p-3 rounded-xl border-2 ${
                  priority === p.value
                    ? "border-primary bg-primary/10"
                    : "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
                }`}
              >
                <Ionicons
                  name={p.icon as any}
                  size={16}
                  color={priority === p.value ? "#6366f1" : "#9ca3af"}
                />
                <Text
                  className={`ml-1 font-medium ${
                    priority === p.value
                      ? "text-primary"
                      : "text-gray-500"
                  }`}
                >
                  {p.label}
                </Text>
              </Pressable>
            ))}
          </View>
        </View>
 
        {/* زر الإرسال */}
        <Pressable
          onPress={handleSubmit}
          className="bg-primary p-4 rounded-xl items-center active:opacity-80"
        >
          <Text className="text-white font-bold text-base">إنشاء المهمة</Text>
        </Pressable>
      </View>
    </ScrollView>
  );
}

الخطوة 7: إنشاء شاشة الإحصائيات

أنشئ ملف app/(tabs)/stats.tsx:

import { View, Text, ScrollView } from "react-native";
import { useState, useCallback } from "react";
import { useFocusEffect } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { getTasks, Task } from "@/lib/database";
 
function StatCard({
  icon,
  label,
  value,
  color,
}: {
  icon: string;
  label: string;
  value: number | string;
  color: string;
}) {
  return (
    <View className="bg-white dark:bg-gray-800 p-4 rounded-2xl flex-1 items-center shadow-sm">
      <Ionicons name={icon as any} size={28} color={color} />
      <Text className="text-2xl font-bold mt-2 dark:text-white">{value}</Text>
      <Text className="text-xs text-gray-500 mt-1">{label}</Text>
    </View>
  );
}
 
export default function StatsScreen() {
  const [tasks, setTasks] = useState<Task[]>([]);
 
  useFocusEffect(
    useCallback(() => {
      setTasks(getTasks());
    }, [])
  );
 
  const total = tasks.length;
  const completed = tasks.filter((t) => t.completed).length;
  const pending = total - completed;
  const highPriority = tasks.filter(
    (t) => t.priority === "high" && !t.completed
  ).length;
  const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
 
  return (
    <ScrollView className="flex-1 bg-gray-50 dark:bg-gray-900 p-4">
      <Text className="text-2xl font-bold mb-6 dark:text-white">
        الإحصائيات
      </Text>
 
      <View className="flex-row gap-3 mb-3">
        <StatCard
          icon="list"
          label="الإجمالي"
          value={total}
          color="#6366f1"
        />
        <StatCard
          icon="checkmark-circle"
          label="مكتملة"
          value={completed}
          color="#22c55e"
        />
      </View>
 
      <View className="flex-row gap-3 mb-6">
        <StatCard
          icon="time"
          label="قيد الانتظار"
          value={pending}
          color="#f59e0b"
        />
        <StatCard
          icon="alert-circle"
          label="أولوية عالية"
          value={highPriority}
          color="#ef4444"
        />
      </View>
 
      {/* معدل الإنجاز */}
      <View className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm">
        <Text className="text-lg font-semibold mb-4 dark:text-white">
          معدل الإنجاز
        </Text>
        <View className="h-4 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
          <View
            className="h-full bg-primary rounded-full"
            style={{ width: `${completionRate}%` }}
          />
        </View>
        <Text className="text-center text-2xl font-bold mt-3 text-primary">
          {completionRate}%
        </Text>
      </View>
 
      {/* توزيع الأولويات */}
      <View className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm mt-4">
        <Text className="text-lg font-semibold mb-4 dark:text-white">
          توزيع الأولويات
        </Text>
        {([
          { key: "high", label: "عالية" },
          { key: "medium", label: "متوسطة" },
          { key: "low", label: "منخفضة" },
        ]).map((p) => {
          const count = tasks.filter((t) => t.priority === p.key).length;
          const pct = total > 0 ? Math.round((count / total) * 100) : 0;
          const colors = {
            high: "#ef4444",
            medium: "#f59e0b",
            low: "#22c55e",
          };
          return (
            <View key={p.key} className="flex-row items-center mb-3">
              <Text className="w-20 text-gray-600 dark:text-gray-400">
                {p.label}
              </Text>
              <View className="flex-1 h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden mx-3">
                <View
                  className="h-full rounded-full"
                  style={{
                    width: `${pct}%`,
                    backgroundColor: colors[p.key as keyof typeof colors],
                  }}
                />
              </View>
              <Text className="text-sm text-gray-500 w-8">{count}</Text>
            </View>
          );
        })}
      </View>
    </ScrollView>
  );
}

الخطوة 8: إضافة دمج الكاميرا لمرفقات المهام

أنشئ ملف app/camera.tsx لشاشة الكاميرا:

import { View, Text, Pressable } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";
import { useRef } from "react";
import { router, useLocalSearchParams } from "expo-router";
import { updateTaskImage } from "@/lib/database";
 
export default function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const cameraRef = useRef<CameraView>(null);
  const { taskId } = useLocalSearchParams<{ taskId: string }>();
 
  if (!permission) return <View />;
 
  if (!permission.granted) {
    return (
      <View className="flex-1 items-center justify-center bg-black">
        <Text className="text-white text-lg mb-4">
          يجب منح صلاحية الوصول للكاميرا
        </Text>
        <Pressable
          onPress={requestPermission}
          className="bg-primary px-6 py-3 rounded-xl"
        >
          <Text className="text-white font-bold">منح الصلاحية</Text>
        </Pressable>
      </View>
    );
  }
 
  const takePicture = async () => {
    if (!cameraRef.current) return;
 
    const photo = await cameraRef.current.takePictureAsync();
    if (photo && taskId) {
      updateTaskImage(parseInt(taskId), photo.uri);
      router.back();
    }
  };
 
  return (
    <View className="flex-1">
      <CameraView ref={cameraRef} className="flex-1" facing="back">
        <View className="flex-1 justify-end items-center pb-10">
          <Pressable
            onPress={takePicture}
            className="w-20 h-20 rounded-full bg-white border-4 border-gray-300 active:opacity-70"
          />
        </View>
      </CameraView>
    </View>
  );
}

أضف مسار الكاميرا إلى التخطيط الجذري:

<Stack.Screen
  name="camera"
  options={{
    presentation: "fullScreenModal",
    headerShown: false,
  }}
/>

الخطوة 9: إعداد الإشعارات الفورية

أنشئ ملف lib/notifications.ts:

import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
 
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});
 
export async function registerForPushNotifications(): Promise<string | null> {
  const { status: existingStatus } =
    await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;
 
  if (existingStatus !== "granted") {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
 
  if (finalStatus !== "granted") {
    return null;
  }
 
  if (Platform.OS === "android") {
    await Notifications.setNotificationChannelAsync("default", {
      name: "default",
      importance: Notifications.AndroidImportance.MAX,
    });
  }
 
  const token = (await Notifications.getExpoPushTokenAsync()).data;
  return token;
}
 
export async function scheduleTaskReminder(
  taskTitle: string,
  dueDate: Date
): Promise<string> {
  const trigger = new Date(dueDate);
  trigger.setHours(trigger.getHours() - 1);
 
  const id = await Notifications.scheduleNotificationAsync({
    content: {
      title: "تذكير بالمهمة",
      body: `"${taskTitle}" موعد تسليمها قريب!`,
      sound: true,
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DATE,
      date: trigger,
    },
  });
 
  return id;
}

الخطوة 10: الإعداد للإنتاج

حدّث app.json لنشر الإنتاج:

{
  "expo": {
    "name": "TaskFlow",
    "slug": "taskflow",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/images/icon.png",
    "scheme": "taskflow",
    "userInterfaceStyle": "automatic",
    "newArchEnabled": true,
    "splash": {
      "image": "./assets/images/splash-icon.png",
      "resizeMode": "contain",
      "backgroundColor": "#6366f1"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.yourcompany.taskflow",
      "infoPlist": {
        "NSCameraUsageDescription": "يستخدم TaskFlow الكاميرا لإرفاق صور بالمهام."
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/images/adaptive-icon.png",
        "backgroundColor": "#6366f1"
      },
      "package": "com.yourcompany.taskflow",
      "permissions": ["CAMERA", "RECEIVE_BOOT_COMPLETED", "SCHEDULE_EXACT_ALARM"]
    },
    "plugins": [
      "expo-router",
      "expo-camera",
      "expo-notifications",
      "expo-sqlite"
    ]
  }
}

الخطوة 11: البناء والنشر مع EAS

ثبّت EAS CLI وأعدّ البناء:

npm install -g eas-cli
eas login
eas build:configure

هذا ينشئ ملف eas.json:

{
  "cli": {
    "version": ">= 3.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  },
  "submit": {
    "production": {}
  }
}

ابنِ لكلا المنصتين:

# بناء التطوير (للاختبار على أجهزة حقيقية)
eas build --profile development --platform all
 
# بناء الإنتاج
eas build --profile production --platform all

أرسل إلى متاجر التطبيقات:

# متجر Apple
eas submit --platform ios
 
# متجر Google Play
eas submit --platform android

الخطوة 12: تفعيل التحديثات عبر الهواء

من أقوى ميزات Expo هي تحديثات OTA — ادفع تحديثات JavaScript دون المرور بمراجعة متجر التطبيقات:

npx expo install expo-updates

أضف إلى app.json:

{
  "expo": {
    "updates": {
      "url": "https://u.expo.dev/your-project-id"
    },
    "runtimeVersion": {
      "policy": "appVersion"
    }
  }
}

ادفع تحديثاً:

eas update --branch production --message "إصلاح خلل في ترتيب المهام"

يحصل المستخدمون على التحديث عند فتح التطبيق التالي — بدون مراجعة متجر التطبيقات.


اختبار التطبيق

شغّل خادم التطوير:

npx expo start

اختبر على جهازك:

  1. iOS: امسح رمز QR بتطبيق الكاميرا
  2. Android: امسح رمز QR بتطبيق Expo Go

اختبر السيناريوهات التالية:

  • إنشاء مهمة جديدة بأولويات مختلفة
  • تعليم المهام كمكتملة/غير مكتملة
  • حذف مهمة
  • التحقق من تحديث صفحة الإحصائيات بشكل صحيح
  • اختبار الوضع الداكن بتبديل إعدادات الجهاز
  • السحب للتحديث في قائمة المهام

استكشاف الأخطاء

"Metro bundler is not running"

npx expo start --clear

أنماط NativeWind لا تعمل

تأكد من استيراد global.css في app/_layout.tsx وأن babel.config.js يتضمن إعدادات NativeWind.

أخطاء SQLite على الويب

Expo SQLite يعمل فقط على iOS و Android. لدعم الويب، استخدم expo-sqlite/next أو حل تخزين آخر.

الكاميرا لا تعمل في Expo Go

بعض الوحدات الأصلية تحتاج بناء تطوير. شغّل:

eas build --profile development --platform ios

الخطوات التالية

الآن بعد أن لديك تطبيق موبايل يعمل، فكّر في هذه التحسينات:

  • مزامنة سحابية — أضف Supabase أو Firebase للمزامنة بين الأجهزة
  • مصادقة بيومترية — استخدم expo-local-authentication لبصمة الإصبع/Face ID
  • ودجت — أنشئ ودجت للشاشة الرئيسية مع expo-widgets
  • حركات — أضف انتقالات مع react-native-reanimated
  • أولوية للعمل دون اتصال — نفّذ نظام مزامنة للعمل بدون إنترنت

الخلاصة

لقد بنيت تطبيق موبايل متعدد المنصات كاملاً باستخدام Expo SDK 52 و React Native و TypeScript. يتضمن التطبيق توجيهاً قائماً على الملفات مع Expo Router، وتخزين بيانات محلي مع SQLite، ودمج الكاميرا، وإشعارات فورية، وهو جاهز للنشر على كلا متجري التطبيقات.

نضج Expo بشكل كبير في 2026، مما جعله الخيار الأول لمطوري React الذين يدخلون عالم الموبايل. مع EAS Build والتحديثات عبر الهواء، أصبحت عملية النشر سلسة كنشر تطبيق ويب. الجمع بين أداء React Native وتجربة مطوّر Expo يجعل بناء تطبيقات الموبايل الإنتاجية أسرع من أي وقت مضى.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على دليل 2026 لنشر Laravel + Vue: من Docker إلى GitHub Actions وCoolify.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

بناء وكلاء الذكاء الاصطناعي من الصفر باستخدام TypeScript: إتقان نمط ReAct مع Vercel AI SDK

تعلّم كيفية بناء وكلاء الذكاء الاصطناعي من الأساس باستخدام TypeScript. يغطي هذا الدليل التعليمي نمط ReAct، واستدعاء الأدوات، والاستدلال متعدد الخطوات، وحلقات الوكلاء الجاهزة للإنتاج مع Vercel AI SDK.

35 د قراءة·