Aller au contenu principal

Mise en page Flexbox

Disposer les éléments avec Flexbox, éviter les barres du téléphone avec SafeAreaView et faire défiler un contenu trop long avec ScrollView.

Objectifs de la séance​

  • comprendre les deux axes de Flexbox (flexDirection, justifyContent, alignItems) ;
  • espacer les éléments avec gap ;
  • limiter la largeur du contenu sur grand écran avec maxWidth et alignSelf ;
  • protéger le contenu des barres du téléphone avec SafeAreaView ;
  • faire défiler l'écran avec ScrollView et contentContainerStyle.

Petite vérification​

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

  1. lancer l'application dans le navigateur et compter les boutons de réponse affichés
  2. dans src/app/index.tsx, citer l'attribut que doit porter chaque <BoutonReponse> généré par map
  3. ouvrir la console du navigateur (F12) et vérifier qu'aucun avertissement au sujet des clés (« unique key ») n'apparaît
📌 Réponse attendue

Notions théoriques​

Flexbox : deux axes​

En React Native, tout composant View organise ses enfants avec Flexbox. L'idée : un conteneur dispose ses enfants le long d'un axe principal, et les aligne sur l'axe secondaire (perpendiculaire).

PropriétéRôle
flexDirectiondirection de l'axe principal : "column" (de haut en bas, valeur par défaut) ou "row" (de gauche à droite)
justifyContentrépartition le long de l'axe principal
alignItemsalignement sur l'axe secondaire
gapespace entre deux enfants consécutifs
flexWrap"wrap" : les enfants passent à la ligne quand la place manque
flex: 1l'élément occupe tout l'espace disponible
Différence avec le CSS

Sur le Web, flex-direction vaut row par défaut. En React Native, la valeur par défaut est column : les enfants d'une View s'empilent donc verticalement sans rien écrire.

Valeurs courantes de justifyContent (avec flexDirection: "row", trois enfants A, B, C) :

ValeurRésultat
"flex-start" (défaut)[A][B][C]
"center" [A][B][C]
"flex-end" [A][B][C]
"space-between"[A] [B] [C]

alignItems accepte "flex-start", "center", "flex-end" et "stretch" (valeur par défaut : les enfants s'étirent sur toute la largeur de l'axe secondaire).

gap : l'espace entre les éléments​

Au lieu d'ajouter un marginBottom à chaque enfant, on règle l'écart une seule fois sur le parent avec gap :

contenu: { gap: 16 }, // 16 d'écart entre chaque enfant du conteneur

Le code est plus court, et un nouvel élément inséré ne casse pas les marges.

Largeur maximale et centrage​

Sur un grand écran (navigateur, tablette), un contenu qui s'étire sur toute la largeur devient illisible. On limite sa largeur avec maxWidth, et on le centre avec alignSelf: "center" :

contenu: { width: "100%", maxWidth: 600, alignSelf: "center" }

width: "100%" signifie « toute la largeur disponible », maxWidth: 600 « mais jamais plus de 600 ». Sur un téléphone étroit, le contenu occupe tout l'écran ; sur un grand écran, il reste centré.

SafeAreaView : respecter les zones réservées​

Un téléphone possède des zones que l'application ne doit pas recouvrir : l'encoche (notch), la barre d'état, la barre de navigation du bas ou l'indicateur de retour à l'accueil. Le composant SafeAreaView ajoute automatiquement une marge intérieure égale à ces zones.

import { SafeAreaView } from "react-native-safe-area-context";

<SafeAreaView style={styles.ecran} edges={["bottom"]}> ... </SafeAreaView>
  • on l'importe depuis react-native-safe-area-context (bibliothèque déjà installée par le modèle) ;
  • edges indique quels bords protéger : ici seulement "bottom", car la barre de titre de la pile d'écrans (Stack) gère déjà le haut de l'écran.
Un contenu caché sous la barre système

Sans SafeAreaView, le dernier bouton de la liste peut se retrouver sous la barre du bas d'un téléphone : il est affiché mais impossible à toucher. Ce défaut n'apparaît pas dans le navigateur d'un ordinateur, d'où l'importance de tester aussi sur un vrai appareil.

ScrollView : faire défiler un contenu​

Si le contenu dépasse la hauteur de l'écran (une question longue, un petit téléphone), il est coupé. ScrollView ajoute le défilement. Elle possède deux propriétés de style :

PropriétéS'applique à
stylele cadre de la ScrollView elle-même
contentContainerStylele contenu qui défile (marges, gap, largeur maximale...)

Les marges et la mise en page du contenu se règlent donc dans contentContainerStyle. (Pour de très longues listes, on utilisera plus tard FlatList, qui n'affiche que les éléments visibles.)


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

Vous allez améliorer la mise en page de l'écran d'accueil : titre et badge de niveau sur une ligne, espacement régulier, contenu protégé des barres du téléphone, défilement et largeur limitée. Vous êtes toujours sur la branche feature/composants.

Étape 1 — Ajouter un badge de niveau​

Dans src/app/index.tsx, importez ScrollView et SafeAreaView (vous les utiliserez plus loin) :

import { ScrollView, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

Entourez ensuite le titre d'une View appelée entete et ajoutez à côté un badge qui affiche le niveau de la question :


Bonne pratique - Regrouper pour mettre en page

Pour placer deux éléments côte à côte, on les regroupe dans une View dédiée : c'est ce conteneur qui reçoit les règles Flexbox, pas les éléments eux-mêmes.

Étape 2 — Disposer le titre et le badge sur une ligne​

Par défaut, les enfants de entete s'empilent (axe column). Dans StyleSheet, ajoutez le style entete : complétez la ligne qui les place côte à côte :


Bonne pratique - Penser en axes

Retenez la règle : justifyContent agit sur l'axe de la direction choisie, alignItems sur l'axe perpendiculaire. En row, justifyContent répartit horizontalement et alignItems aligne verticalement.

Étape 3 — Écarter et styler le badge​

La valeur "space-between" écarte le titre (à gauche) et le badge (à droite) au maximum. Ajoutez le style du badge. Complétez la ligne qui lui donne un fond bleu, pris dans la palette :

paddingHorizontal et paddingVertical règlent la marge intérieure à gauche/droite et en haut/bas. La ligne overflow: "hidden" garantit que le fond coloré respecte l'arrondi des coins, notamment sur iOS.


Bonne pratique - Réutiliser la palette

Le texte du badge utilise COULEURS.carte (blanc) sur COULEURS.primaire (bleu) : toujours vérifier qu'un texte est bien lisible sur son fond.

Étape 4 — Remplacer les marges par gap​

Les composants CarteQuestion et BoutonReponse portent chacun un marginBottom (16 et 8). Supprimez la ligne marginBottom: 16, du style carte de CarteQuestion.tsx et la ligne marginBottom: 8, du style bouton de BoutonReponse.tsx. Les écarts seront désormais gérés par le parent :


Bonne pratique - Un composant ne gère pas ses marges extérieures

Un composant réutilisable ne doit pas décider de l'espace qui l'entoure : cette décision revient au parent (avec gap). Sans marginBottom dans BoutonReponse, ce bouton s'adapte à n'importe quelle liste.

Étape 5 — Protéger les barres du téléphone avec SafeAreaView​

Remplacez la View racine de l'écran par un SafeAreaView qui protège le bas de l'écran :


Bonne pratique - Ne protéger que les bords utiles

Le haut de l'écran est déjà géré par la barre de titre du Stack. Protéger un bord déjà géré ajouterait un espace inutile : edges permet de choisir précisément.

Étape 6 — Rendre l'écran défilant avec ScrollView​

À l'intérieur du SafeAreaView, placez tout le contenu (en-tête, carte, réponses) dans un ScrollView. Les marges et le gap se règlent dans contentContainerStyle :

Le style ecran ne garde plus que flex: 1 et la couleur de fond (le padding passe dans contenu). Pensez à fermer </ScrollView> puis </SafeAreaView> à la fin du JSX. Réduisez la hauteur de la fenêtre du navigateur : l'écran défile.


Bonne pratique - style ou contentContainerStyle ?

Une erreur fréquente est de mettre le padding dans style : il s'appliquerait alors au cadre fixe, et non au contenu qui défile. Marges et espacements du contenu : toujours contentContainerStyle.

Étape 7 — Limiter la largeur sur grand écran​

Agrandissez la fenêtre du navigateur : le contenu s'étire sur toute la largeur, ce qui est peu lisible. Le style contenu limite déjà sa largeur à 600 ; complétez la ligne qui le centre :


Bonne pratique - Penser aux grands écrans dès le départ

Une application mobile s'affiche aussi sur tablette et dans un navigateur. Une largeur maximale garde l'interface confortable sur tous les formats, sans code spécifique par appareil.

Étape 8 — Enregistrer votre travail​


Bonne pratique - Un commit lisible

Le message de commit énumère les trois techniques du jour : en relisant l'historique, on sait immédiatement dans quel commit retrouver la mise en page de l'écran.

📌 Une solution

Test de mémorisation/compréhension​


Quelle est la valeur par défaut de flexDirection en React Native ?


Avec flexDirection: "row", quelle propriété répartit les enfants horizontalement ?


Que produit justifyContent: "space-between" sur une ligne contenant un titre et un badge ?


Pourquoi utiliser SafeAreaView ?


Dans un ScrollView, où place-t-on le padding et le gap du contenu qui défile ?


À quoi servent width: "100%", maxWidth: 600 et alignSelf: "center" ensemble ?



Exercices complémentaires​

Exercice A : le badge à gauche​

Modifiez justifyContent du style entete pour que le titre et le badge soient collés à gauche l'un à côté de l'autre (valeur "flex-start"), puis ajoutez gap: 12 pour les séparer. Remettez ensuite "space-between".

Exercice B : sans SafeAreaView​

Remplacez temporairement SafeAreaView par View. Sur le navigateur, voyez-vous une différence ? Pourquoi ce changement serait-il visible sur un téléphone récent sans bouton physique ?


Ce qu'il faut retenir​

NotionRésumé
flexDirection"column" par défaut (vertical), "row" pour aligner horizontalement.
justifyContent / alignItemsAxe principal / axe secondaire (center, space-between...).
gapÉcart entre les enfants : réglé sur le parent plutôt que par des marges.
maxWidth + alignSelf: "center"Limite la largeur et centre le contenu sur grand écran.
SafeAreaViewÉvite les zones système ; edges choisit les bords protégés.
ScrollViewFait défiler le contenu ; contentContainerStyle règle ce contenu.

Aperçu de la prochaine séance​

L'écran est propre, mais il manque une illustration pour rendre les questions plus parlantes. Dans la prochaine séance, vous ajouterez un champ facultatif image au type Question, vous apprendrez à charger des images locales avec require() et à les afficher avec le composant expo-image. Vous terminerez le tutoriel en fusionnant votre branche feature/composants dans main par une Pull Request.