Skip to Content

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.tsx rendant <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.
  • tabBarIcon reçoit color/size — laissez la barre décider (états actif/inactif).
  • L’ordre des Tabs.Screen définit l’ordre visuel ; title sert 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.