TP Guidé : Application Multi-écrans
Ce TP vous guide pas à pas pour construire une application mobile complète avec 3 onglets et navigation entre écrans. Vous allez mettre en pratique tous les concepts vus dans cette séance.
Ce que vous allez construire
Une application "Mon Carnet" avec trois onglets :
- Accueil : liste de contacts avec navigation vers un écran détail
- Favoris : liste des contacts favoris
- Paramètres : écran de configuration
L'application utilise :
- Tab Navigation (3 onglets)
- Stack Navigation (liste vers détail)
- Composants primitifs (View, Text, Image, ScrollView)
- StyleSheet et Flexbox
Pré-requis
Assurez-vous d'avoir créé un projet Expo et d'avoir Expo Go fonctionnel sur votre téléphone (voir la page Installation).
Étape 1 : Créer le projet
Si vous n'avez pas encore de projet, créez-en un :
npx create-expo-app@latest mon-carnet
cd mon-carnet
Nettoyez le projet en supprimant les fichiers d'exemple :
npm run reset-project
Cette commande remet le dossier app/ à un état minimal.
Étape 2 : Structure de fichiers
Créez la structure suivante dans le dossier app/ :
app/
├── _layout.tsx → Layout racine (Stack)
├── (tabs)/
│ ├── _layout.tsx → Configuration des onglets
│ ├── index.tsx → Onglet Accueil (liste de contacts)
│ ├── favorites.tsx → Onglet Favoris
│ └── settings.tsx → Onglet Paramètres
└── contact/
└── [id].tsx → Écran détail d'un contact
Étape 3 : Les données
Créez un fichier data/contacts.js à la racine du projet pour les données partagées :
// data/contacts.js
export const contacts = [
{
id: '1',
name: 'Alice Dupont',
role: 'Développeuse frontend',
phone: '06 12 34 56 78',
email: 'alice@example.com',
avatar: 'https://i.pravatar.cc/150?img=1',
favorite: true,
},
{
id: '2',
name: 'Bob Martin',
role: 'Designer UI/UX',
phone: '06 23 45 67 89',
email: 'bob@example.com',
avatar: 'https://i.pravatar.cc/150?img=3',
favorite: false,
},
{
id: '3',
name: 'Claire Rousseau',
role: 'Chef de projet',
phone: '06 34 56 78 90',
email: 'claire@example.com',
avatar: 'https://i.pravatar.cc/150?img=5',
favorite: true,
},
{
id: '4',
name: 'David Chen',
role: 'Développeur backend',
phone: '06 45 67 89 01',
email: 'david@example.com',
avatar: 'https://i.pravatar.cc/150?img=7',
favorite: false,
},
{
id: '5',
name: 'Eva Schmidt',
role: 'Data scientist',
phone: '06 56 78 90 12',
email: 'eva@example.com',
avatar: 'https://i.pravatar.cc/150?img=9',
favorite: true,
},
];
Étape 4 : Layout racine
Le layout racine utilise un Stack pour combiner les onglets et l'écran détail :
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
{/* Les onglets sont le premier écran */}
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
{/* L'écran détail s'affiche au-dessus des onglets */}
<Stack.Screen
name="contact/[id]"
options={{ title: 'Détail du contact' }}
/>
</Stack>
);
}
Étape 5 : Configuration des onglets
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#0066cc',
tabBarInactiveTintColor: '#999',
tabBarStyle: {
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Contacts',
tabBarIcon: ({ color, size }) => (
<Ionicons name="people" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="favorites"
options={{
title: 'Favoris',
tabBarIcon: ({ color, size }) => (
<Ionicons name="star" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Paramètres',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
Étape 6 : Composant ContactCard (réutilisable)
Créez un composant réutilisable pour afficher un contact. Ce composant sera utilisé dans l'onglet Accueil et l'onglet Favoris :
// components/ContactCard.jsx
import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
export default function ContactCard({ contact }) {
const router = useRouter();
return (
<Pressable
style={styles.card}
onPress={() => router.push(`/contact/${contact.id}`)}
>
<Image
source={{ uri: contact.avatar }}
style={styles.avatar}
/>
<View style={styles.info}>
<Text style={styles.name}>{contact.name}</Text>
<Text style={styles.role}>{contact.role}</Text>
</View>
{contact.favorite && (
<Text style={styles.star}>*</Text>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 12,
marginBottom: 8,
borderRadius: 12,
borderWidth: 1,
borderColor: '#e0e0e0',
},
avatar: {
width: 50,
height: 50,
borderRadius: 25,
},
info: {
flex: 1,
marginLeft: 12,
},
name: {
fontSize: 16,
fontWeight: 'bold',
},
role: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
star: {
fontSize: 20,
color: '#f5a623',
},
});
Pressable pour les éléments cliquables
Pressable est le composant React Native pour rendre un élément interactif. Il remplace onClick du web par onPress. Quand l'utilisateur appuie sur la carte, router.push() navigue vers l'écran détail du contact.
Étape 7 : Onglet Accueil (liste de contacts)
// app/(tabs)/index.tsx
import { View, Text, ScrollView, StyleSheet } from 'react-native';
import { contacts } from '../../data/contacts';
import ContactCard from '../../components/ContactCard';
export default function HomeScreen() {
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Mes contacts</Text>
<Text style={styles.subtitle}>{contacts.length} contacts</Text>
{contacts.map(contact => (
<ContactCard key={contact.id} contact={contact} />
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#666',
marginBottom: 16,
},
});
Étape 8 : Onglet Favoris
// app/(tabs)/favorites.tsx
import { View, Text, ScrollView, StyleSheet } from 'react-native';
import { contacts } from '../../data/contacts';
import ContactCard from '../../components/ContactCard';
export default function FavoritesScreen() {
// Filtrer les contacts favoris
const favoriteContacts = contacts.filter(contact => contact.favorite);
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Favoris</Text>
<Text style={styles.subtitle}>
{favoriteContacts.length} favori{favoriteContacts.length > 1 ? 's' : ''}
</Text>
{favoriteContacts.length === 0 ? (
<View style={styles.empty}>
<Text style={styles.emptyText}>Aucun favori pour le moment</Text>
</View>
) : (
favoriteContacts.map(contact => (
<ContactCard key={contact.id} contact={contact} />
))
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#666',
marginBottom: 16,
},
empty: {
alignItems: 'center',
paddingVertical: 40,
},
emptyText: {
fontSize: 16,
color: '#999',
},
});
Étape 9 : Onglet Paramètres
// app/(tabs)/settings.tsx
import { View, Text, ScrollView, StyleSheet } from 'react-native';
function SettingsRow({ label, value }) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<Text style={styles.rowValue}>{value}</Text>
</View>
);
}
export default function SettingsScreen() {
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Paramètres</Text>
<Text style={styles.sectionTitle}>COMPTE</Text>
<View style={styles.section}>
<SettingsRow label="Nom" value="Mon Carnet" />
<View style={styles.separator} />
<SettingsRow label="Version" value="1.0.0" />
<View style={styles.separator} />
<SettingsRow label="SDK Expo" value="52" />
</View>
<Text style={styles.sectionTitle}>AFFICHAGE</Text>
<View style={styles.section}>
<SettingsRow label="Theme" value="Automatique" />
<View style={styles.separator} />
<SettingsRow label="Langue" value="Francais" />
</View>
<Text style={styles.sectionTitle}>A PROPOS</Text>
<View style={styles.section}>
<SettingsRow label="Cours" value="R4A11" />
<View style={styles.separator} />
<SettingsRow label="Seance" value="1 - Fondamentaux" />
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f2f2f7',
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 24,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#666',
marginBottom: 8,
marginTop: 16,
marginLeft: 16,
},
section: {
backgroundColor: '#fff',
borderRadius: 12,
paddingHorizontal: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
rowLabel: {
fontSize: 17,
},
rowValue: {
fontSize: 17,
color: '#999',
},
separator: {
height: 1,
backgroundColor: '#e0e0e0',
},
});
Étape 10 : Écran détail d'un contact
// app/contact/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text, Image, ScrollView, StyleSheet } from 'react-native';
import { contacts } from '../../data/contacts';
export default function ContactDetailScreen() {
const { id } = useLocalSearchParams();
// Trouver le contact par son ID
const contact = contacts.find(c => c.id === id);
// Gérer le cas où le contact n'existe pas
if (!contact) {
return (
<View style={styles.centered}>
<Text style={styles.errorText}>Contact non trouvé</Text>
</View>
);
}
return (
<ScrollView style={styles.container}>
{/* En-tête avec avatar */}
<View style={styles.header}>
<Image
source={{ uri: contact.avatar }}
style={styles.avatar}
/>
<Text style={styles.name}>{contact.name}</Text>
<Text style={styles.role}>{contact.role}</Text>
</View>
{/* Informations de contact */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>COORDONNEES</Text>
<View style={styles.infoCard}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Telephone</Text>
<Text style={styles.infoValue}>{contact.phone}</Text>
</View>
<View style={styles.separator} />
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Email</Text>
<Text style={styles.infoValue}>{contact.email}</Text>
</View>
</View>
</View>
{/* Statut favori */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>STATUT</Text>
<View style={styles.infoCard}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Favori</Text>
<Text style={styles.infoValue}>
{contact.favorite ? 'Oui' : 'Non'}
</Text>
</View>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f2f2f7',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
fontSize: 18,
color: '#999',
},
header: {
alignItems: 'center',
paddingVertical: 32,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
avatar: {
width: 120,
height: 120,
borderRadius: 60,
marginBottom: 16,
},
name: {
fontSize: 24,
fontWeight: 'bold',
},
role: {
fontSize: 16,
color: '#666',
marginTop: 4,
},
infoSection: {
padding: 16,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#666',
marginBottom: 8,
marginLeft: 16,
},
infoCard: {
backgroundColor: '#fff',
borderRadius: 12,
paddingHorizontal: 16,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
infoLabel: {
fontSize: 17,
},
infoValue: {
fontSize: 17,
color: '#0066cc',
},
separator: {
height: 1,
backgroundColor: '#e0e0e0',
},
});
Résultat attendu
En lançant npx expo start et en scannant le QR code, vous devriez voir :
-
Onglet Contacts : une liste de 5 contacts. En appuyant sur un contact, l'écran détail s'affiche avec les coordonnées complètes. Le geste de swipe (ou bouton retour) ramène à la liste.
-
Onglet Favoris : les 3 contacts marqués comme favoris (Alice, Claire, Eva). Même navigation vers le détail.
-
Onglet Paramètres : les informations de l'application organisées en sections avec un style inspiré des réglages iOS.
Erreurs courantes
Problèmes fréquents
1. "Text strings must be rendered within a Text component"
Vous avez mis du texte directement dans une View. Encapsulez-le dans <Text>.
2. L'image ne s'affiche pas
Vérifiez que vous avez spécifié width et height dans le style de l'Image.
3. Les onglets ne s'affichent pas
Vérifiez que le dossier s'appelle bien (tabs) avec les parenthèses, et que le _layout.tsx existe dans ce dossier.
4. L'écran détail ne s'affiche pas
Vérifiez que le fichier s'appelle [id].tsx (avec les crochets) et qu'il est déclaré dans le layout racine _layout.tsx.
5. Erreur d'import
Les chemins d'import relatifs (../../data/contacts) dépendent de la position du fichier. Vérifiez le nombre de ../ nécessaires.
Pour aller plus loin
Si vous avez terminé le TP et qu'il vous reste du temps :
- Ajoutez un champ de recherche dans l'onglet Contacts pour filtrer par nom
- Améliorez le style des cartes de contact (ombres, icônes)
- Ajoutez des animations :
Pressablesupportestylecomme fonction pour réagir à l'état pressé
// Effet de pression sur la carte
<Pressable
style={({ pressed }) => [
styles.card,
pressed && { opacity: 0.7 }
]}
onPress={() => router.push(`/contact/${contact.id}`)}
>
{/* ... */}
</Pressable>