Aller au contenu principal

Afficher des images avec expo-image

Illustrer les questions avec des images locales, un champ facultatif et le composant expo-image, puis fusionner le tutoriel dans main.

Objectifs de la séance​

  • ajouter des images au projet et les charger avec require() ;
  • comprendre pourquoi require() n'accepte pas un chemin calculé ;
  • afficher une image avec le composant Image d'expo-image (contentFit, transition) ;
  • ajouter un champ facultatif image? au type Question ;
  • afficher un élément seulement si une condition est vraie (&&) ;
  • fusionner la branche feature/composants dans main par une Pull Request.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. lancer l'application dans le navigateur et lire le texte du badge affiché à droite du titre
  2. réduire la hauteur de la fenêtre du navigateur et vérifier que l'écran défile
  3. citer le composant qui empêche le contenu de passer sous la barre du bas du téléphone
📌 Réponse attendue

TP pour réfléchir et résoudre des problèmes​

Les questions sont plus parlantes avec une illustration. Votre mission : afficher une image au-dessus de la carte, uniquement pour les questions qui en possèdent une. Vous êtes toujours sur la branche feature/composants.

Étape 1 — Ajouter les images au projet​

Trois illustrations libres de droits vous sont fournies. Enregistrez chacune d'elles (clic droit, Enregistrer l'image sous...) dans le dossier assets/images/questions/ du projet (à créer) :

cadenas.pnghamecon.pngwifi.png
Un cadenasUne enveloppe et un hameçonLe symbole du Wi-Fi

Vérifiez que les trois fichiers apparaissent bien dans l'explorateur de VS Code, dans assets/images/questions/.


Bonne pratique - Ranger les images par usage

Le sous-dossier questions/ regroupe les illustrations des questions. Plus tard, d'autres types d'images (icônes, logos) auront leur propre sous-dossier.

Étape 2 — Un champ facultatif dans le type Question​

Toutes les questions n'ont pas d'illustration. Dans src/types/quiz.ts, ajoutez au type Question un champ facultatif : le point d'interrogation après le nom signifie « peut être absent ».


Bonne pratique - Faciliter l'évolution des données

Un champ facultatif ne casse aucune des 24 autres questions : TypeScript n'exige image nulle part. On ajoute une information sans modifier l'existant.

Étape 3 — Associer chaque nom de fichier à son image​

Créez le fichier src/data/imagesLocales.ts. Les images locales se chargent avec la fonction require(), dont le chemin doit être écrit en toutes lettres : on ne peut pas le construire à partir d'un nom variable. On rassemble donc les trois require() dans un « dictionnaire » qui associe un nom de fichier à son image :


Bonne pratique - Un seul endroit pour les images

Ce fichier est la seule liste des images de l'application. Ajouter une image revient à ajouter une ligne ici, sans toucher aux composants.

Étape 4 — Le composant IllustrationQuestion​

Créez src/components/IllustrationQuestion.tsx. Il reçoit le nom de l'image, retrouve l'image dans le dictionnaire et l'affiche avec le composant Image du paquet expo-image (déjà installé par le modèle de projet). Complétez d'abord le garde-fou : si le nom est inconnu, le composant n'affiche rien plutôt que de faire planter l'application :

Terminez le composant par l'affichage de l'image et ses styles. Complétez la ligne qui règle l'ajustement de l'image dans son cadre :


Bonne pratique - Ne jamais planter pour une image

Une image manquante est un défaut visuel, pas une raison d'arrêter le quiz. Le garde-fou renvoie null (« rien à afficher ») et le reste de l'écran continue de fonctionner.

Étape 5 — Associer des images à trois questions​

Dans src/data/questionsLocales.ts, ajoutez le champ image aux questions 1, 10 et 19 (une par thème). Complétez la question 1, puis faites de même avec "hamecon.png" pour la question 10 et "wifi.png" pour la question 19. Placez la ligne image juste avant reponses.


Bonne pratique - TypeScript contrôle les champs, pas les noms de fichiers

Le type string accepte n'importe quel texte : une faute de frappe ("cadnas.png") n'est pas détectée par tsc. C'est pourquoi le composant prévoit le cas d'un nom inconnu.

Étape 6 — Afficher l'image seulement si elle existe​

Dans src/app/index.tsx, importez IllustrationQuestion puis placez-le entre l'en-tête et la carte. Comme toutes les questions n'ont pas d'image, on utilise l'opérateur && :

Le cadenas s'affiche au-dessus de la carte de la première question. Faites un essai : remplacez temporairement "cadenas.png" par "inconnue.png" dans la question 1 : plus d'image, mais aucune erreur (c'est le garde-fou de l'étape 4). Remettez ensuite "cadenas.png".


Bonne pratique - Tester ce que les types ne garantissent pas

Chaque fois qu'une valeur dépend d'un nom ou d'un indice calculé, prévoyez le cas « introuvable ». Ce réflexe évite de nombreux plantages.

Étape 7 — Enregistrer votre travail​


Bonne pratique - Vérifier avant d'envoyer

tsc et npm test doivent passer avant de proposer la branche à la fusion : une Pull Request qui casse les tests fait perdre du temps au relecteur.

Étape 8 — Fusionner le tutoriel dans main​

Le tutoriel est terminé : tout le travail de feature/composants doit entrer dans main.

  1. Sur GitHub, ouvrez une Pull Request pour la branche feature/composants (bandeau Compare & pull request), titre : « Composants et styles de l'écran du quiz ».
  2. Relisez l'onglet Files changed : vous devez y voir les composants, les images, les données et l'écran modifié (la palette a déjà été fusionnée), et aucun fichier inattendu.
  3. Cliquez sur Merge pull request, Confirm merge, puis Delete branch.
  4. Dans le terminal, récupérez la fusion et supprimez la branche locale :

Bonne pratique - Terminer proprement un tutoriel

Après la fusion, vous êtes sur main, à jour, sans branche inutile : l'état idéal pour démarrer le tutoriel suivant sur une nouvelle branche.

📌 Une solution

Notions théoriques​

Images locales et require()​

Une image locale est un fichier du projet, intégré à l'application au moment de sa construction. On la charge avec require() :

const image = require("@/assets/images/questions/cadenas.png");

Le chemin doit être écrit en toutes lettres : le bundler (l'outil qui assemble l'application) lit le code avant son exécution pour y repérer les images. Une écriture comme require("@/assets/images/" + nom) est donc impossible, puisque nom n'existe qu'à l'exécution.

La solution : un « dictionnaire » qui associe un nom à son require() :

export const IMAGES_LOCALES: Record<string, number> = {
"cadenas.png": require("@/assets/images/questions/cadenas.png"),
};
  • Record<string, number> décrit un objet dont toutes les clés sont des textes et toutes les valeurs des nombres (require() renvoie ici un numéro qui désigne l'image) ;
  • on retrouve une image avec IMAGES_LOCALES["cadenas.png"], ou avec une variable : IMAGES_LOCALES[nomImage].

Le composant Image d'expo-image​

expo-image fournit un composant Image plus complet que celui de React Native : mise en cache, transitions, formats modernes.

PropriétéRôle
sourcel'image à afficher (ici le résultat du require())
styletaille, arrondi... : obligatoire de donner une largeur et une hauteur à une image
contentFitcomment l'image occupe son cadre : "contain" (entière, sans déformation), "cover" (remplit en rognant)
transitiondurée (en millisecondes) du fondu d'apparition
accessibilityIgnoresInvertColorssur iOS, empêche l'inversion des couleurs de l'image en mode d'accessibilité

Un champ facultatif : image?​

Dans le type Question, image?: string signifie que le champ peut être absent. TypeScript le traite comme string | undefined : avant de l'utiliser, il faut vérifier qu'il existe, sinon l'éditeur signale une erreur.

Affichage conditionnel avec &&​

En JSX, condition && <Composant /> affiche le composant seulement si la condition est vraie (si elle est fausse, rien n'est affiché) :

{question.image !== undefined && (
<IllustrationQuestion nomImage={question.image} />
)}

Dans la parenthèse, TypeScript sait que question.image est un texte (et non undefined) : le test vérifie donc à la fois la logique et le type.

Attention au 0 avec &&

Avec {nombre && <Composant />}, si nombre vaut 0, React affiche... le chiffre 0. Préférez une comparaison explicite (nombre > 0 && ..., ou !== undefined comme ici).


Test de mémorisation/compréhension​


Pourquoi require("@/assets/images/" + nom) est-il impossible ?


Que signifie le type Record<string, number> pour IMAGES_LOCALES ?


Que signifie image?: string dans le type Question ?


Que fait contentFit="contain" sur une image expo-image ?


Quel est l'effet de {question.image !== undefined && (<IllustrationQuestion ... />)} ?


Pourquoi IllustrationQuestion renvoie-t-il null quand le nom d'image est inconnu ?


Que se passe-t-il avec {nombre && <Composant />} quand nombre vaut 0 ?



Exercices complémentaires​

Exercice A : remplir le cadre​

Remplacez contentFit="contain" par contentFit="cover" et comparez le résultat. Quelle est la différence visible sur la bordure de l'image ? Remettez ensuite "contain".

Exercice B : ajouter une quatrième image​

Ajoutez au dictionnaire une image de votre choix (que vous avez le droit d'utiliser), par exemple pare-feu.png, puis associez-la à une question de la banque. Quelles sont toutes les lignes à modifier ou à ajouter ?


Ce qu'il faut retenir​

NotionRésumé
require()Charge une image locale ; le chemin doit être écrit en toutes lettres (jamais calculé).
Dictionnaire d'imagesRecord<string, number> : associe un nom de fichier à son require().
expo-imageComposant Image avec contentFit, transition et mise en cache.
image?Champ facultatif : absent pour certaines questions (string | undefined).
condition && <X />Affiche X seulement si la condition est vraie.
Garde-fouPrévoir le cas « introuvable » (undefined) pour ne jamais faire planter l'écran.

Aperçu de la prochaine séance​

L'écran du quiz est complet, mais figé : un clic sur une réponse n'écrit encore qu'un message dans la console. Dans le prochain tutoriel, vous allez donner vie à l'application avec l'état de React : le bouton choisi se colore en vert ou en rouge, la correction s'affiche, on passe à la question suivante et le score se calcule, avec useState, puis useReducer et useEffect pour un chronomètre.