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 avecexpo-status-bar; les dimensions avecuseWindowDimensions(jamais l’importDimensionsstatique) ; les différences de plateforme avecPlatform.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 dereact-native(déprécié, iOS seulement !) et celui dereact-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 tabletteuseWindowDimensions 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ère | ScrollView | FlatList |
|---|---|---|
| Contenu | Borné, connu (un article, un formulaire, un écran de résultat) | Données (liste de N items) |
| Rendu | TOUT, immédiatement | Virtualisé |
| Hétérogène | Parfait (mélange libre de composants) | Possible mais contraint |
| Dans Drill | Écran résultat, réglages, explication | Thè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.