Construire une Application Mobile Multiplateforme avec Expo, React Native et TypeScript en 2026

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Un seul code, deux plateformes. Expo est devenu la manière par défaut de construire des applications React Native en 2026. Avec le routage basé sur les fichiers, le support des modules natifs et les mises à jour over-the-air, il supprime la complexité du développement mobile tout en conservant les capacités natives complètes. Dans ce tutoriel, vous construirez une application complète de gestion de tâches fonctionnant sur iOS et Android.

Ce que vous allez construire

Une application TaskFlow complète — une application de gestion de tâches comprenant :

  • Navigation basée sur les fichiers avec Expo Router v4
  • Interface élégante avec NativeWind (Tailwind CSS pour React Native)
  • Persistance locale des données avec Expo SQLite
  • Intégration de la caméra pour les pièces jointes
  • Notifications push pour les rappels de tâches
  • Support du mode sombre
  • Configuration prête pour le déploiement

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • Un smartphone (iOS ou Android) avec l'application Expo Go installée
  • Des connaissances de base en React et TypeScript
  • VS Code avec l'extension Expo Tools (recommandé)
  • Un compte Expo (gratuit sur expo.dev)

Étape 1 : Créer le projet Expo

Commencez par créer un nouveau projet Expo avec le dernier SDK :

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

Cela crée un projet avec le routage basé sur les fichiers déjà configuré. Examinons la structure :

TaskFlow/
├── app/                    # Routes basées sur les fichiers
│   ├── (tabs)/             # Groupe de navigation par onglets
│   │   ├── _layout.tsx     # Configuration des onglets
│   │   ├── index.tsx       # Onglet Accueil
│   │   └── explore.tsx     # Onglet Explorer
│   ├── _layout.tsx         # Layout racine
│   └── +not-found.tsx      # Page 404
├── assets/                 # Images, polices
├── components/             # Composants réutilisables
├── constants/              # Thème, configuration
└── package.json

Installez les dépendances supplémentaires nécessaires :

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

Étape 2 : Configurer NativeWind (Tailwind CSS)

NativeWind apporte les classes utilitaires de Tailwind CSS à React Native. Configurez-le :

npx expo install nativewind tailwindcss react-native-reanimated

Créez 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: [],
};

Créez global.css à la racine du projet :

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

Mettez à jour babel.config.js pour inclure le plugin NativeWind :

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

Maintenant, mettez à jour app/_layout.tsx pour importer les styles globaux :

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>
  );
}

Étape 3 : Configurer la base de données avec Expo SQLite

Créez lib/database.ts pour la persistance locale des données :

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]);
}

Initialisez la base de données dans app/_layout.tsx :

import { useEffect } from "react";
import { initDatabase } from "@/lib/database";
 
export default function RootLayout() {
  const colorScheme = useColorScheme();
 
  useEffect(() => {
    initDatabase();
  }, []);
 
  // ... reste du layout
}

Étape 4 : Construire la navigation par onglets

Mettez à jour app/(tabs)/_layout.tsx pour les onglets de notre application :

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: "Tâches",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="list" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="add"
        options={{
          title: "Ajouter",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="add-circle" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="stats"
        options={{
          title: "Statistiques",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="stats-chart" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Étape 5 : Créer l'écran de liste des tâches

Remplacez le contenu de 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: "Haute",
    medium: "Moyenne",
    low: "Basse",
  };
 
  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">
              Échéance : {new Date(task.dueDate).toLocaleDateString("fr")}
            </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">Mes Tâches</Text>
        <Text className="text-sm text-gray-500 mt-1">
          {completedCount}/{tasks.length} terminées
        </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">
              Aucune tâche pour le moment
            </Text>
            <Text className="text-gray-400 text-sm mt-1">
              {"Appuyez sur l'onglet + pour ajouter votre première tâche"}
            </Text>
          </View>
        }
      />
    </View>
  );
}

Étape 6 : Créer l'écran d'ajout de tâche

Créez 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: "Basse", color: "bg-green-500", icon: "arrow-down" },
  { value: "medium", label: "Moyenne", color: "bg-yellow-500", icon: "remove" },
  { value: "high", label: "Haute", 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("Erreur", "Le titre de la tâche est requis");
      return;
    }
 
    addTask(title.trim(), description.trim(), priority);
    Alert.alert("Succès", "Tâche créée !", [
      { text: "OK", 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">
          Nouvelle Tâche
        </Text>
 
        {/* Champ Titre */}
        <View className="mb-4">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            Titre *
          </Text>
          <TextInput
            value={title}
            onChangeText={setTitle}
            placeholder="Que faut-il faire ?"
            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>
 
        {/* Champ Description */}
        <View className="mb-4">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            Description
          </Text>
          <TextInput
            value={description}
            onChangeText={setDescription}
            placeholder="Ajoutez des détails..."
            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>
 
        {/* Sélecteur de Priorité */}
        <View className="mb-6">
          <Text className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
            Priorité
          </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>
 
        {/* Bouton Soumettre */}
        <Pressable
          onPress={handleSubmit}
          className="bg-primary p-4 rounded-xl items-center active:opacity-80"
        >
          <Text className="text-white font-bold text-base">Créer la Tâche</Text>
        </Pressable>
      </View>
    </ScrollView>
  );
}

Étape 7 : Créer l'écran des statistiques

Créez 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">
        Statistiques
      </Text>
 
      <View className="flex-row gap-3 mb-3">
        <StatCard
          icon="list"
          label="Total"
          value={total}
          color="#6366f1"
        />
        <StatCard
          icon="checkmark-circle"
          label="Terminées"
          value={completed}
          color="#22c55e"
        />
      </View>
 
      <View className="flex-row gap-3 mb-6">
        <StatCard
          icon="time"
          label="En attente"
          value={pending}
          color="#f59e0b"
        />
        <StatCard
          icon="alert-circle"
          label="Haute priorité"
          value={highPriority}
          color="#ef4444"
        />
      </View>
 
      {/* Taux de complétion */}
      <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">
          Taux de complétion
        </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>
 
      {/* Répartition par priorité */}
      <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">
          Répartition par priorité
        </Text>
        {([
          { key: "high", label: "Haute" },
          { key: "medium", label: "Moyenne" },
          { key: "low", label: "Basse" },
        ]).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>
  );
}

Étape 8 : Ajouter l'intégration caméra pour les pièces jointes

Créez app/camera.tsx pour un écran caméra modal :

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">
          {"L'autorisation de la caméra est requise"}
        </Text>
        <Pressable
          onPress={requestPermission}
          className="bg-primary px-6 py-3 rounded-xl"
        >
          <Text className="text-white font-bold">{"Accorder l'autorisation"}</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>
  );
}

Ajoutez la route caméra au layout racine :

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

Étape 9 : Configurer les notifications push

Créez 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: "Rappel de tâche",
      body: `"${taskTitle}" arrive bientôt à échéance !`,
      sound: true,
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DATE,
      date: trigger,
    },
  });
 
  return id;
}

Étape 10 : Configurer pour la production

Mettez à jour app.json pour le déploiement en production :

{
  "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 utilise la caméra pour joindre des photos aux tâches."
      }
    },
    "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"
    ]
  }
}

Étape 11 : Compiler et déployer avec EAS

Installez EAS CLI et configurez les builds :

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

Cela crée eas.json :

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

Compilez pour les deux plateformes :

# Build de développement (pour tester sur de vrais appareils)
eas build --profile development --platform all
 
# Build de production
eas build --profile production --platform all

Soumettez aux stores :

# App Store Apple
eas submit --platform ios
 
# Google Play Store
eas submit --platform android

Étape 12 : Activer les mises à jour over-the-air

L'une des fonctionnalités les plus puissantes d'Expo est les mises à jour OTA — poussez des mises à jour JavaScript sans passer par la revue des stores :

npx expo install expo-updates

Ajoutez à app.json :

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

Poussez une mise à jour :

eas update --branch production --message "Correction du tri des tâches"

Les utilisateurs reçoivent la mise à jour au prochain lancement — aucune revue de store nécessaire.


Tester votre implémentation

Lancez le serveur de développement :

npx expo start

Testez sur votre appareil :

  1. iOS : Scannez le QR code avec l'application Appareil photo
  2. Android : Scannez le QR code avec l'application Expo Go

Testez les scénarios suivants :

  • Créer une nouvelle tâche avec différentes priorités
  • Marquer des tâches comme terminées/non terminées
  • Supprimer une tâche
  • Vérifier que la page de statistiques se met à jour correctement
  • Tester le mode sombre en changeant les paramètres de votre appareil
  • Tirer pour actualiser la liste des tâches

Dépannage

"Metro bundler is not running"

npx expo start --clear

Les styles NativeWind ne s'appliquent pas

Assurez-vous que global.css est importé dans app/_layout.tsx et que babel.config.js inclut le preset NativeWind.

Erreurs SQLite sur le web

Expo SQLite fonctionne uniquement sur iOS et Android. Pour le support web, utilisez expo-sqlite/next ou une autre solution de stockage.

La caméra ne fonctionne pas dans Expo Go

Certains modules natifs nécessitent un build de développement. Exécutez :

eas build --profile development --platform ios

Prochaines étapes

Maintenant que vous avez une application mobile fonctionnelle, envisagez ces améliorations :

  • Synchronisation cloud — Ajoutez Supabase ou Firebase pour la synchronisation multi-appareils
  • Authentification biométrique — Utilisez expo-local-authentication pour l'empreinte digitale/Face ID
  • Widgets — Créez des widgets d'écran d'accueil avec expo-widgets
  • Animations — Ajoutez des transitions avec react-native-reanimated
  • Offline-first — Implémentez une file de synchronisation pour le mode hors connexion

Conclusion

Vous avez construit une application mobile multiplateforme complète avec Expo SDK 52, React Native et TypeScript. L'application comprend le routage basé sur les fichiers avec Expo Router, la persistance locale avec SQLite, l'intégration caméra, les notifications push, et est prête pour le déploiement sur les deux stores.

Expo a considérablement mûri en 2026, devenant le choix par défaut pour les développeurs React qui se lancent dans le mobile. Avec EAS Build et les mises à jour over-the-air, le workflow de déploiement est aussi fluide que celui d'une application web. La combinaison des performances de React Native avec l'expérience développeur d'Expo rend la construction d'applications mobiles de production plus rapide que jamais.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Hub de Tutoriels AI SDK : Votre Guide Complet pour Construire des Applications IA.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·