Chapitre 5.2 — Tabs : les 4 onglets de Drill
Où on en est : vous savez empiler des écrans. Structure réelle d’une app mobile : des onglets persistants en bas, chacun avec sa propre pile. On construit la charpente définitive de Drill.
⏱️ TL;DR — Un dossier groupe
(tabs)avec son_layout.tsxrendant<Tabs>: Accueil, Thèmes, Historique, Réglages. Les parenthèses n’apparaissent pas dans l’URL. Chaque onglet garde son état ; les icônes viennent d’@expo/vector-icons. L’écran de quiz, lui, vivra hors des tabs (plein écran, pas de barre) — la structure hybride classique.
🎯 Objectifs
- Structurer l’app en groupe (tabs) + écrans hors tabs.
- Configurer Tabs : icônes, couleurs, labels — la barre de Drill.
- Comprendre la persistance d’état par onglet et le comportement du back Android.
- Décider ce qui va DANS les tabs et ce qui n’y va pas (le quiz).
L’architecture cible
app/
├── _layout.tsx → Stack racine
├── (tabs)/ → le groupe d'onglets (invisible dans l'URL)
│ ├── _layout.tsx → <Tabs> : la barre du bas
│ ├── index.tsx → onglet Accueil "/"
│ ├── themes.tsx → onglet Thèmes "/themes"
│ ├── history.tsx → onglet Historique "/history"
│ └── settings.tsx → onglet Réglages "/settings"
└── quiz.tsx → l'écran de jeu — HORS tabs, plein écran "/quiz"Pourquoi le quiz hors des tabs ? Pendant une partie, la barre d’onglets serait une fuite de
concentration (et un moyen de tricher en allant voir un thème !). Le pattern : les tabs pour
la « maison », des écrans du Stack racine par-dessus pour les flux immersifs (jeu, onboarding,
paywall…). Le Stack racine empile /quiz AU-DESSUS du groupe (tabs) entier.
// app/_layout.tsx — le Stack racine connaît deux « écrans » : les tabs et le quiz
import { Stack } from 'expo-router'
export default function RootLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="quiz" options={{ gestureEnabled: false }} />
</Stack>
)
}(gestureEnabled: false : on ne quitte pas une partie d’un revers de pouce accidentel — on
gérera l’abandon proprement au chapitre 5.4.)
La barre d’onglets
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: { backgroundColor: '#101b21', borderTopColor: '#1e3a45' },
tabBarActiveTintColor: '#58c4dc',
tabBarInactiveTintColor: '#7a8b94',
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Accueil',
tabBarIcon: ({ color, size }) => <Ionicons name="home" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="themes"
options={{
title: 'Thèmes',
tabBarIcon: ({ color, size }) => <Ionicons name="library" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: 'Historique',
tabBarIcon: ({ color, size }) => <Ionicons name="stats-chart" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Réglages',
tabBarIcon: ({ color, size }) => <Ionicons name="settings" color={color} size={size} />,
}}
/>
</Tabs>
)
}Notes de terrain :
@expo/vector-icons(inclus dans le template) embarque Ionicons, MaterialIcons, Feather… Parcourez icons.expo.fyi pour choisir.tabBarIconreçoitcolor/size— laissez la barre décider (états actif/inactif).- L’ordre des Tabs.Screen définit l’ordre visuel ;
titlesert de label. - 3 à 5 onglets, jamais plus — au-delà, c’est un menu qu’il vous faut.
Persistance et back : les comportements à connaître
- Chaque onglet garde son état en changeant d’onglet (les écrans restent montés). Le scroll de l’historique survit à un aller-retour par Réglages.
- Un onglet peut contenir sa propre pile (un dossier avec _layout Stack à l’intérieur du groupe) — on le fera au chapitre 5.3 pour Thèmes → détail d’un thème.
- Back Android dans les tabs : revient à l’onglet initial (index) avant de quitter l’app — comportement système standard, ne le combattez pas.
⚠️ Piège — Un fichier présent dans
(tabs)/mais absent des<Tabs.Screen>apparaît QUAND MÊME comme onglet (auto-enregistrement !). Inversement, pour avoir un écran dans le groupe sans onglet visible :<Tabs.Screen name="secret" options={{ href: null }} />. Gardez le dossier (tabs) minimal et explicite.
📱 Essaie sur ton téléphone — Montez la structure complète. Vérifiez : la barre est sombre avec l’actif cyan, chaque onglet garde son scroll, et
/quiz(poussé depuis un bouton de l’accueil) recouvre TOUT — barre comprise.
✏️ Exercices
1. L’accueil de Drill doit afficher : salutation, le bouton « ⚡ Quiz rapide » (pousse /quiz), et les 2-3 derniers scores (données en dur). Construisez-le avec le kit UI.
✅ Solution
// app/(tabs)/index.tsx
export default function HomeScreen() {
return (
<Screen scroll>
<AppText variant="title">Drill</AppText>
<AppText variant="caption" className="mb-6">Prêt à te challenger ?</AppText>
<Link href="/quiz" asChild>
<Button label="⚡ Quiz rapide" onPress={() => {}} />
</Link>
<AppText variant="heading" className="mb-2 mt-8">Dernières sessions</AppText>
<Card className="mb-2"><AppText>Symfony — 870 pts</AppText></Card>
<Card><AppText>PHP — 640 pts</AppText></Card>
</Screen>
)
}(Le onPress vide disparaîtra : Link asChild navigue déjà. Les vraies données arrivent en P8.)
2. Ajoutez un badge de pastille (« 3 ») sur l’icône Historique — la prop existe, trouvez-la.
✅ Solution
<Tabs.Screen
name="history"
options={{
title: 'Historique',
tabBarBadge: 3,
tabBarBadgeStyle: { backgroundColor: '#ef4444', color: '#fff' },
tabBarIcon: …,
}}
/>tabBarBadge (nombre ou string). En vrai usage : nombre de nouveaux records non consultés —
on le branchera sur les données en Partie 8.
3. Pourquoi ne PAS mettre l’écran quiz dans les tabs avec la barre masquée
(tabBarStyle: { display: 'none' } conditionnel), pattern qu’on croise dans des tutos ?
✅ Solution
Masquer la barre par style laisse le quiz DANS la logique des onglets : back Android renvoie vers d’autres tabs, l’onglet « actif » devient incohérent, la barre clignote pendant les transitions. Le modèle mental correct : le jeu est un flux au-dessus de la maison → un écran du Stack racine. La structure de navigation doit refléter la structure de l’expérience.
🧠 Quiz
1. Que fait le nom de dossier (tabs) — avec parenthèses ?
Réponse
C’est un groupe : il organise les fichiers et porte un _layout (le navigateur Tabs) sans
apparaître dans l’URL — (tabs)/history.tsx = route /history.
2. Comment l’écran de quiz recouvre-t-il la barre d’onglets ?
Réponse
Il est déclaré dans le Stack racine, frère du groupe (tabs) : le push l’empile au-dessus du navigateur d’onglets entier, barre comprise.
3. L’état d’un onglet survit-il au passage à un autre onglet ?
Réponse
Oui — les écrans d’onglets restent montés (scroll, useState conservés). Pour rafraîchir au retour : useFocusEffect.
4. Comment cacher un fichier du groupe tabs sans le supprimer de la navigation ?
Réponse
<Tabs.Screen name="x" options={{ href: null }} /> — l’écran reste navigable
programmatiquement mais n’a pas d’onglet.
5. D’où viennent les icônes de la barre ?
Réponse
@expo/vector-icons (Ionicons & co, inclus au template), via la prop tabBarIcon qui reçoit
color/size gérés par la barre. Catalogue : icons.expo.fyi.
👉 Chapitre suivant : 5.3 — Routes dynamiques —
/theme/symfony : un écran, N thèmes.