Skip to Content

Chapitre 3.6 — S’adapter à l’écran

Où on en est : dernier fondamental. Vos écrans doivent vivre avec les encoches, les barres système, mille tailles d’écran Android — et choisir correctement entre ScrollView et FlatList.

⏱️ TL;DR — Les safe areas (encoche, barre de gestes) se gèrent avec react-native-safe-area-context (SafeAreaView / useSafeAreaInsets) ; la StatusBar avec expo-status-bar ; les dimensions avec useWindowDimensions (jamais l’import Dimensions statique) ; les différences de plateforme avec Platform.select. Et la règle ScrollView vs FlatList : contenu borné → ScrollView, données → FlatList.

🎯 Objectifs

  • Ne plus jamais avoir de contenu sous l’encoche ou la barre de gestes.
  • Styler la barre de statut proprement (thème sombre de Drill).
  • Écrire des layouts qui réagissent aux dimensions et à la plateforme.
  • Trancher définitivement ScrollView vs FlatList.

Les safe areas — l’écran n’est pas un rectangle

Un écran Android moderne a une caméra perforée en haut et une barre de gestes en bas. Poser du contenu à top: 0 le glisse SOUS la caméra. Les « zones sûres » sont fournies par react-native-safe-area-context (déjà installé par le template Expo Router) :

// Écran simple : le composant import { SafeAreaView } from 'react-native-safe-area-context' export default function HomeScreen() { return ( <SafeAreaView style={{ flex: 1 }} edges={['top']}> {/* contenu jamais sous l'encoche */} </SafeAreaView> ) }
// Contrôle fin : le hook — pour du padding sur mesure import { useSafeAreaInsets } from 'react-native-safe-area-context' const insets = useSafeAreaInsets() <View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom }}>

edges choisit les côtés protégés — un écran sous un header de navigation n’a pas besoin du top (le header le gère, Partie 5) ; l’écran de quiz plein écran de Drill, si.

⚠️ Piège — Il existe DEUX SafeAreaView : celui de react-native (déprécié, iOS seulement !) et celui de react-native-safe-area-context. Importez toujours depuis la lib. Un import auto de VS Code qui choisit le mauvais = encoche gérée sur iPhone, ignorée sur votre Android.

StatusBar — la bande du haut

L’heure, la batterie… s’affichent au-dessus de VOTRE app. Vous contrôlez leur style :

// app/_layout.tsx — global à l'app import { StatusBar } from 'expo-status-bar' <> <Stack screenOptions={{ headerShown: false }} /> <StatusBar style="light" /> {/* icônes claires sur notre fond sombre */} </>

style="light" (icônes claires), "dark", ou "auto" (suit le thème système). Drill étant sombre : light. Le composant d’expo-status-bar est déclaratif — l’écran le plus profond qui en rend un gagne.

Dimensions — toujours le hook

import { useWindowDimensions } from 'react-native' const { width, height } = useWindowDimensions() const isLandscape = width > height const columns = width >= 600 ? 2 : 1 // grille de thèmes sur tablette

useWindowDimensions se met à jour (rotation, écran pliable, mode multi-fenêtres Android). Le vieil import statique Dimensions.get('window') capture une valeur figée au premier appel — bugs garantis sur pliables. Le hook, toujours.

Pour la responsivité, rappel de nos armes (détail en Partie 4) : flexbox d’abord (flex, pourcentages, gap), dimensions ensuite, et pas de media queries — des conditions JS comme columns ci-dessus.

Platform — les différences assumées

import { Platform } from 'react-native' // Valeur ponctuelle const behavior = Platform.OS === 'ios' ? 'padding' : 'height' // Bloc de styles par plateforme const styles = StyleSheet.create({ shadow: Platform.select({ android: { elevation: 4 }, // l'ombre Android ios: { shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 8 }, default: {}, }), })

Notre cours est Android-first, mais on écrit cross-platform quand ça ne coûte rien — et Platform.select est exactement ce « ne coûte rien ». Les ombres sont l’exemple canonique : elevation côté Android, shadow* côté iOS.

Il existe aussi les extensions de fichier : Component.android.tsx / Component.ios.tsx — Metro choisit seul. Réservé aux composants qui divergent vraiment (rare dans nos apps).

ScrollView vs FlatList — l’arbitrage final

CritèreScrollViewFlatList
ContenuBorné, connu (un article, un formulaire, un écran de résultat)Données (liste de N items)
RenduTOUT, immédiatementVirtualisé
HétérogèneParfait (mélange libre de composants)Possible mais contraint
Dans DrillÉcran résultat, réglages, explicationThèmes, historique

La question à se poser : « est-ce que ça grandit avec les données ? » Oui → FlatList. Non → ScrollView. Et jamais de FlatList DANS une ScrollView de même axe (le warning VirtualizedLists should never be nested vous le rappellera) : la virtualisation a besoin de contrôler son viewport — utilisez ListHeaderComponent pour le contenu au-dessus de la liste.

📱 Essaie sur ton téléphone — Passez l’écran d’accueil de Drill en SafeAreaView (edges={['top']}), ajoutez <StatusBar style="light" /> au layout, et faites pivoter le téléphone : orientation: "portrait" d’app.json (chapitre 2.5) tient bon — le paysage est hors sujet pour un quiz.

✏️ Exercices

1. L’écran de quiz de Drill est plein écran (pas de header). Posez la structure : safe areas correctes en haut ET en bas, fond sombre jusque sous la barre de gestes (le fond « déborde », le contenu non).

✅ Solution

export default function QuizScreen() { const insets = useSafeAreaInsets() return ( <View style={{ flex: 1, backgroundColor: '#0b1216' }}> <View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom }}> {/* timer, question, réponses */} </View> </View> ) }

Le fond est porté par la View externe (plein écran, sous les barres) ; le padding des insets protège le contenu seulement. Avec SafeAreaView seul, le fond s’arrêterait à l’encoche — moche.

2. La grille des thèmes doit passer à 2 colonnes au-dessus de 600 dp de large. FlatList a une prop pour ça — trouvez-la et écrivez le code.

✅ Solution

const { width } = useWindowDimensions() const columns = width >= 600 ? 2 : 1 <FlatList key={columns} // numColumns ne peut pas changer à chaud sans remount numColumns={columns} columnWrapperStyle={columns > 1 ? { gap: 12 } : undefined} data={themes} />

Subtilité réelle : numColumns exige un remount pour changer → key={columns}.

3. Pourquoi Dimensions.get('window') au top-level d’un module est-il un bug en puissance ?

✅ Solution

Il s’exécute une fois à l’import : la valeur est figée. Rotation, pliable déplié, multi-fenêtres → l’app garde les anciennes dimensions. useWindowDimensions est réactif et re-render le composant quand ça change. (Un usage figé est acceptable uniquement pour des constantes vraiment statiques, ex. la densité.)

🧠 Quiz

1. D’où importer SafeAreaView, et pourquoi pas de react-native ?

Réponse

De react-native-safe-area-context. Celui de react-native est déprécié et ne fonctionne que sur iOS — l’encoche Android resterait ignorée.

2. Quand utiliser useSafeAreaInsets plutôt que SafeAreaView ?

Réponse

Pour un contrôle fin : padding sur mesure, fond qui déborde sous les barres avec contenu protégé, ou insets injectés dans un style calculé (tab bar custom, overlay).

3. Comment faire une ombre cross-platform ?

Réponse

Platform.select : elevation (nombre) côté Android, shadowColor/shadowOpacity/shadowRadius côté iOS.

4. Que risque une FlatList imbriquée dans une ScrollView verticale ?

Réponse

La virtualisation casse (le viewport n’est plus contrôlé) — warning « VirtualizedLists should never be nested ». Solution : ListHeaderComponent pour le contenu au-dessus de la liste.

5. Récitez la règle ScrollView vs FlatList.

Réponse

Contenu borné et connu (formulaire, résultat, article) → ScrollView. Contenu qui grandit avec les données → FlatList. « Ça grandit ? » est la seule question.


👉 Partie suivante : Partie 4 — Styling & layout — StyleSheet en profondeur, le flexbox RN, puis le retour de Tailwind avec NativeWind.