Développement Mobile

Accès natif en Expo : caméra, permissions et stockage local

13 دقائق للقراءة

📍 Article principal du parcours : React Native et Expo : créer une application mobile en 2026
Cette leçon fait partie de la série React Native. Pour la vue d’ensemble, commencez par le guide principal.

Sortir du cadre du JavaScript

Une application mobile n’a d’intérêt que si elle exploite ce que le téléphone a de spécial : l’appareil photo, le stockage local, les capteurs, le mode hors connexion. C’est là que React Native dépasse le web. Pour StockPoche, deux capacités natives changent tout : prendre une photo d’un article pour l’identifier d’un coup d’œil, et conserver l’inventaire sur l’appareil pour le consulter même sans réseau, dans une réserve ou un entrepôt mal couvert.

Le SDK Expo expose ces fonctions natives derrière des API JavaScript propres, sans avoir à écrire une ligne de Swift ou de Kotlin. Dans cette leçon, vous demandez les permissions correctement, vous capturez une photo avec expo-camera, vous la rattachez à un produit, et vous persistez l’inventaire avec un stockage clé-valeur. À la fin, StockPoche fonctionnera hors connexion et chaque produit pourra porter sa photo.

🎯 Ce que vous allez apprendre

  • Demander et gérer les permissions natives proprement.
  • Capturer une photo avec expo-camera (composant CameraView).
  • Persister des données hors connexion avec un stockage clé-valeur.
  • Stocker une donnée sensible de façon sécurisée avec expo-secure-store.
  • Comprendre quand un module natif impose un build de développement.

🛠️ Ce que vous allez construire

Deux fonctions natives pour StockPoche : un écran caméra qui photographie un article et renvoie l’image vers sa fiche, et une persistance locale qui sauvegarde l’inventaire sur le téléphone pour qu’il survive à la fermeture de l’app et au manque de réseau.

Prérequis

  • Le projet StockPoche avec la couche de données en place (Gérer l’état et consommer une API).
  • Un téléphone physique : la caméra ne fonctionne pas dans la plupart des émulateurs.
  • Niveau : intermédiaire à avancé. Vous êtes à l’aise avec les hooks et l’asynchrone.
  • ⏱️ Temps estimé : ~50 minutes.

Étape 1 — Comprendre les permissions et les builds natifs

Avant le code, deux notions qui évitent des heures de blocage. D’abord, toute fonction sensible (caméra, position, micro) exige une permission accordée par l’utilisateur à l’exécution. Ensuite, certains modules natifs ne sont pas inclus dans Expo Go : il faut alors un development build, une version de l’app compilée avec vos modules natifs. expo-camera fonctionne dans Expo Go, mais gardez ce principe en tête pour la suite.

On installe la caméra avec la commande qui sélectionne la version compatible avec le SDK.

npx expo install expo-camera

Sur un build natif, les permissions doivent aussi être déclarées dans la configuration. Expo le fait automatiquement via le plugin du module, mais on peut préciser le texte affiché à l’utilisateur dans app.json — un message clair augmente le taux d’acceptation.

{
  "expo": {
    "plugins": [
      ["expo-camera", { "cameraPermission": "StockPoche utilise la caméra pour photographier vos articles." }]
    ]
  }
}

Point d’étapeexpo-camera apparaît dans package.json. Le message de permission est défini. Aucun écran ne change encore : on construit l’écran caméra à l’étape suivante.

Étape 2 — Demander la permission caméra

Le module fournit un hook dédié, useCameraPermissions, qui gère l’état de l’autorisation et la demande. On vérifie l’état au montage de l’écran et on demande l’accès si nécessaire, en affichant un message explicite plutôt qu’un écran vide.

// app/produit/camera.tsx
import { CameraView, useCameraPermissions } from 'expo-camera';
import { Button, Text, View } from 'react-native';

export default function EcranCamera() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission) {
    return <View />; // permission en cours de chargement
  }

  if (!permission.granted) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
        <Text style={{ marginBottom: 12 }}>
          StockPoche a besoin de la caméra pour photographier l'article.
        </Text>
        <Button title="Autoriser la caméra" onPress={requestPermission} />
      </View>
    );
  }

  return <Text>Permission accordée — caméra prête.</Text>;
}

Le hook renvoie l’objet permission (avec son champ granted) et la fonction requestPermission. Tant que permission est null, l’état se charge. S’il n’est pas accordé, on explique pourquoi et on propose le bouton. Cette gestion en trois états — chargement, refusé, accordé — est le motif standard pour toute permission. Ne sautez jamais l’explication : un utilisateur qui ne comprend pas la demande refuse.

Point d’étape — En ouvrant l’écran, la demande système apparaît. Après acceptation, le message « caméra prête » s’affiche. Si vous avez refusé par erreur, réautorisez depuis les réglages du téléphone.

Étape 3 — Capturer une photo

Maintenant que la permission est accordée, affichons l’aperçu de la caméra et capturons une image. CameraView rend le flux ; on lui attache une référence pour appeler takePictureAsync, qui renvoie l’URI du fichier image enregistré sur l’appareil.

import { useRef, useState } from 'react';
import { CameraView } from 'expo-camera';
import { Pressable, StyleSheet, View } from 'react-native';

const cameraRef = useRef<CameraView>(null);
const [photoUri, setPhotoUri] = useState<string | null>(null);

async function prendrePhoto() {
  const photo = await cameraRef.current?.takePictureAsync({ quality: 0.6 });
  if (photo) setPhotoUri(photo.uri);
}

return (
  <View style={{ flex: 1 }}>
    <CameraView ref={cameraRef} style={{ flex: 1 }} facing="back" />
    <Pressable onPress={prendrePhoto} style={styles.declencheur} />
  </View>
);

La référence cameraRef donne accès aux méthodes impératives de la caméra. takePictureAsync({ quality: 0.6 }) capture une image compressée à 60 % — un bon compromis entre netteté et poids du fichier, important sur mobile. L’objet renvoyé contient uri, le chemin local de la photo, que l’on stocke dans l’état pour l’afficher ou l’attacher au produit. La propriété facing="back" sélectionne la caméra arrière, naturelle pour photographier un objet.

Point d’étape — L’aperçu caméra remplit l’écran et appuyer sur le déclencheur capture une photo dont l’URI est stockée. Si l’aperçu reste noir, vérifiez que vous testez sur un appareil physique, pas un émulateur sans caméra.

Étape 4 — Rattacher la photo au produit

Une photo isolée ne sert à rien : il faut la lier au produit. On renvoie l’URI à la fiche via la navigation, puis on l’affiche avec le composant image vu précédemment. La fiche produit récupère l’URI et l’enregistre dans le produit correspondant.

import { Image } from 'expo-image';
import { useRouter } from 'expo-router';

// Après la capture, on revient à la fiche en passant l'URI :
const router = useRouter();
router.back();
// puis on stocke photoUri dans le produit via le Context useProduits()

// Dans la fiche, afficher la photo si elle existe :
{produit.photoUri ? (
  <Image source={{ uri: produit.photoUri }} style={{ width: '100%', height: 200, borderRadius: 12 }} contentFit="cover" />
) : (
  <Text style={{ color: '#999' }}>Aucune photo</Text>
)}

L’URI pointe vers un fichier local de l’appareil ; expo-image l’affiche comme n’importe quelle source. On ajoute un champ optionnel photoUri au type Produit et on le met à jour via le Context. Désormais, chaque article peut porter sa photo — une aide précieuse pour identifier un produit dont le nom seul ne suffit pas, comme une référence de visserie parmi des dizaines.

Étape 5 — Persister l’inventaire hors connexion

Si l’utilisateur ferme l’app, les produits ajoutés localement disparaissent — sauf si on les enregistre sur l’appareil. Pour un stockage clé-valeur simple, on utilise AsyncStorage, le standard de la communauté. Installez-le et écrivez deux fonctions : sauvegarder et charger l’inventaire.

npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Produit } from '@/data/produits';

const CLE = 'stockpoche.inventaire';

export async function sauverInventaire(produits: Produit[]) {
  await AsyncStorage.setItem(CLE, JSON.stringify(produits));
}

export async function chargerInventaireLocal(): Promise<Produit[]> {
  const brut = await AsyncStorage.getItem(CLE);
  return brut ? JSON.parse(brut) : [];
}

AsyncStorage ne stocke que des chaînes : on sérialise donc le tableau avec JSON.stringify à l’écriture, et on le reconstruit avec JSON.parse à la lecture. La clé stockpoche.inventaire identifie notre donnée. Toutes les méthodes sont asynchrones, d’où les await. Appelez sauverInventaire à chaque modification de la liste, et chargerInventaireLocal au démarrage : l’inventaire survit alors à la fermeture et reste consultable sans réseau.

Point d’étape — Ajoutez un produit, fermez complètement l’app, rouvrez-la : le produit est toujours là. Si la liste revient vide, vérifiez que sauverInventaire est bien appelé après chaque ajout.

Étape 6 — Sécuriser une donnée sensible

AsyncStorage convient à des données ordinaires, mais pas à un secret comme un jeton d’authentification : son contenu n’est pas chiffré. Pour cela, Expo fournit expo-secure-store, qui range la donnée dans le trousseau sécurisé du système (Keychain sur iOS, Keystore sur Android).

npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';

// Enregistrer le jeton après connexion
await SecureStore.setItemAsync('token', jeton);

// Le relire au démarrage
const jeton = await SecureStore.getItemAsync('token');

// Le supprimer à la déconnexion
await SecureStore.deleteItemAsync('token');

L’API ressemble à AsyncStorage — setItemAsync, getItemAsync, deleteItemAsync — mais la donnée est chiffrée par le système d’exploitation. La règle est simple : tout ce qui est secret va dans SecureStore, le reste dans AsyncStorage. Ne stockez jamais un jeton ou un mot de passe en clair dans un stockage non chiffré.

Point d’étape — Le jeton se stocke et se relit sans erreur. Sur le simulateur iOS, si SecureStore échoue, c’est souvent un problème de configuration du trousseau : testez sur un appareil réel.

Étape 7 — Choisir une image existante avec expo-image-picker

Toutes les photos ne se prennent pas sur le moment : parfois l’image du produit existe déjà dans la galerie. Le module expo-image-picker ouvre le sélecteur natif et renvoie l’image choisie, avec la même forme de résultat que la caméra. Cela offre une seconde voie pour illustrer un article.

npx expo install expo-image-picker
import * as ImagePicker from 'expo-image-picker';

async function choisirImage() {
  const resultat = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ['images'],
    quality: 0.6,
  });
  if (!resultat.canceled) {
    setPhotoUri(resultat.assets[0].uri);
  }
}

La fonction launchImageLibraryAsync ouvre la galerie du système et attend le choix de l’utilisateur. Le résultat porte un drapeau canceled : s’il est false, l’image retenue se trouve dans resultat.assets[0].uri. On réutilise le même setPhotoUri que pour la caméra, donc tout le reste du flux (affichage, rattachement au produit) fonctionne sans changement. Offrir les deux options — appareil photo et galerie — est l’usage attendu sur mobile.

Point d’étape — Un bouton « Choisir dans la galerie » ouvre le sélecteur et l’image retenue s’affiche comme une photo prise. Si rien ne s’affiche après sélection, vérifiez que vous lisez bien resultat.assets[0].uri et non l’ancien champ déprécié.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
L’aperçu caméra reste noir Test sur émulateur sans caméra Tester sur un appareil physique
La demande de permission ne s’affiche pas Permission déjà refusée précédemment Réautoriser dans les réglages système du téléphone
« Cannot read property takePictureAsync » La référence caméra n’est pas encore prête Utiliser l’optional chaining cameraRef.current?.
Les données ne survivent pas au redémarrage Oubli de sérialiser ou d’appeler la sauvegarde JSON.stringify à l’écriture, sauvegarder à chaque changement
Un module natif plante dans Expo Go Module non inclus dans Expo Go Créer un development build

🌍 Réalités du terrain

Le mode hors connexion n’est pas un luxe : dans beaucoup de contextes professionnels — réserve sans réseau, marché, zone rurale — l’app doit rester pleinement utilisable sans données mobiles. Persister l’inventaire localement avec AsyncStorage, puis synchroniser avec le serveur quand le réseau revient, est un schéma d’architecture qui rend StockPoche réellement exploitable sur le terrain. C’est ce qui distingue une démo d’un outil de travail.

Côté photo, la compression compte double sur mobile : une image à pleine résolution pèse plusieurs mégaoctets, ce qui sature vite le stockage du téléphone et la bande passante au moment de la synchronisation. Le réglage quality: 0.6 et, si besoin, un redimensionnement avant l’envoi (avec expo-image-manipulator) gardent les fichiers légers — un souci concret quand la donnée et l’espace de stockage sont comptés.

✅ Récapitulatif

StockPoche exploite désormais le matériel. Vous gérez les permissions en trois états (chargement, refusé, accordé), capturez une photo avec CameraView et takePictureAsync, la rattachez à un produit, et persistez l’inventaire hors connexion avec AsyncStorage. Vous savez aussi proposer une image issue de la galerie avec expo-image-picker, mettre une donnée sensible à l’abri dans expo-secure-store, et reconnaître quand un module natif impose un build de développement. L’app est complète sur le plan fonctionnel : il ne reste qu’à la livrer, sujet de la dernière leçon.

🧾 Aide-mémoire

Élément Rôle
useCameraPermissions() Demander/vérifier la permission caméra
CameraView Afficher l’aperçu de la caméra
takePictureAsync() Capturer une photo (renvoie une URI)
AsyncStorage Stockage clé-valeur non chiffré
SecureStore Stockage chiffré pour données sensibles
JSON.stringify / JSON.parse Sérialiser pour AsyncStorage
Development build Requis pour les modules natifs hors Expo Go

💪 À vous de jouer

Ajoutez un bouton « Reprendre » sur l’écran caméra qui efface la photo capturée et réaffiche l’aperçu, pour permettre de refaire une prise ratée.

Voir une solution

Affichez conditionnellement l’aperçu ou la photo capturée selon photoUri, avec un bouton qui le remet à null :

{photoUri ? (
  <View style={{ flex: 1 }}>
    <Image source={{ uri: photoUri }} style={{ flex: 1 }} contentFit="cover" />
    <Button title="Reprendre" onPress={() => setPhotoUri(null)} />
  </View>
) : (
  <CameraView ref={cameraRef} style={{ flex: 1 }} facing="back" />
)}

Remettre photoUri à null réaffiche la caméra : l’utilisateur peut refaire la photo autant de fois qu’il veut avant de valider.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Faut-il forcément créer un build de développement pour la caméra ?
R : Non, expo-camera fonctionne dans Expo Go. Le build de développement devient nécessaire pour des modules natifs non inclus dans Expo Go, ou quand vous ajoutez du code natif personnalisé.

Q : Quelle différence entre prendre une photo et choisir dans la galerie ?
R : expo-camera capture une nouvelle photo ; pour piocher une image existante dans la galerie, on utilise expo-image-picker. Les deux renvoient une URI exploitable de la même manière.

Q : AsyncStorage a-t-il une limite de taille ?
R : Il convient à des données modestes (préférences, petit inventaire). Pour de gros volumes ou des requêtes complexes, une base locale comme SQLite (via expo-sqlite) est plus adaptée.

Q : Les photos prises comptent-elles dans le stockage de l’app ?
R : Oui, elles sont écrites dans le répertoire de l’app. Pensez à nettoyer les photos inutilisées et à compresser à la capture pour ne pas saturer l’espace du téléphone au fil du temps.

مشاركة