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

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 TaskFlowCela 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-reanimatedCré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:configureCela 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 allSoumettez 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-updatesAjoutez à 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 startTestez sur votre appareil :
- iOS : Scannez le QR code avec l'application Appareil photo
- 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 --clearLes 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 iosProchaines é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-authenticationpour 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.
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.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.

Better Auth avec Next.js 15 : Le Guide Complet d'Authentification pour 2026
Apprenez à implémenter une authentification complète dans Next.js 15 avec Better Auth. Ce tutoriel couvre email/mot de passe, OAuth, sessions, protection des routes et contrôle d'accès basé sur les rôles.