• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 09/09/2020

Implémentez une navigation avancée

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Maintenant que Redux n'est plus un secret pour vous, :soleil: nous allons pousser la navigation dans notre application encore plus loin. Nous allons créer une barre d'onglets, aussi appelée TabBar dans le mobile. Pour l'instant, on se contentera de deux onglets : un pour la recherche de films et un pour afficher les films favoris. 

Pour créer une TabBar, on aurait pu utiliser le component React Native, à savoir TabBarIOS, mais, comme son nom l'indique, il n'est compatible qu'avec iOS. On ne va pas s'embêter à utiliser ce component sur iOS et à en créer un autre sur Android.

Au lieu de cela, on va utiliser la librairie react-navigation. Vous savez, c'est la librairie que l'on a utilisée pour passer de la vue recherche à la vue détail du film. La librairie possède un composant TabNavigator qui fait exactement ce dont on a besoin.

TabNavigator

Avant de créer une barre d'onglets, il faut comprendre ce que c'est exactement. Quand on y réfléchit bien, une barre d'onglets n'est qu'un simple menu, où chaque entrée du menu affiche une vue. Dans notre application, l'onglet "recherche" va afficher la vue "recherche de film", et l'onglet "favoris", la vue "favoris". On associe donc un onglet à une vue.

Si vous regardez la documentation du TabNavigator, dans le premier exemple, on peut voir comment créer un TabNavigator : 

export default TabNavigator({
Home: { screen: HomeScreen },
Settings: { screen: SettingsScreen },
})

Ici, on crée une barre d'onglets avec deux onglets et, à chaque onglet, on associe une vue : "HomeScreen" et "SettingsScreen". Vous voyez ? C'est exactement ce que l'on a dit précédemment : un onglet = une vue.

On va reprendre l'exemple de la documentation et créer la barre d'onglets dans notre projet. On a déjà la vue recherche, il nous manque la vue Favoris. Faites donc comme moi et créez un component Favorites qui correspondra à notre vue Favoris :

// Components/Favorites.js
import React from 'react'
import { StyleSheet, Text } from 'react-native'
class Favorites extends React.Component {
render() {
return (
<Text>Mes Favoris</Text>
)
}
}
const styles = StyleSheet.create({})
export default Favorites

Stop, n'allez pas plus loin ! On remplira cette vue quand notre TabNavigator sera en place.

Ajout d'un TabNavigator

Un TabNavigator est un système de navigation et, rappelez-vous, on a créé un fichier exprès pour gérer la navigation. La création d'un TabNavigator passe par la fonction  createBottomTabNavigator de React Navigation. Placez-vous dans le fichier /Navigation/Navigation.js et créez un TabNavigator avec les vues recherche et favoris : 

// Navigation/Navigation.js
import { createStackNavigator, createAppContainer, createBottomTabNavigator } from 'react-navigation'
import Search from '../Components/Search'
import FilmDetail from '../Components/FilmDetail'
import Favorites from '../Components/Favorites'
const SearchStackNavigator = createStackNavigator({
Search: {
screen: Search,
navigationOptions: {
title: 'Rechercher'
}
},
FilmDetail: {
screen: FilmDetail
}
})
const MoviesTabNavigator = createBottomTabNavigator({
Search: {
screen: Search
},
Favorites: {
screen: Favorites
}
})
export default createAppContainer(SearchStackNavigator)

Notre TabNavigator devient notre système de navigation principale dans l'application. C'est donc ce nouveau component que l'on va ajouter dans notre AppContainer. Remplacez :

export default createAppContainer(SearchStackNavigator)

Par :

export default createAppContainer(MoviesTabNavigator)

Dans le fichier App.js, où la navigation est utilisée, on a toujours :

// App.js
import React from 'react'
import Navigation from './Navigation/Navigation'
import { Provider } from 'react-redux'
import Store from './Store/configureStore'
export default class App extends React.Component {
render() {
return (
<Provider store={Store}>
<Navigation/>
</Provider>
)
}
}

Normalement, c'est tout bon, l'élément importé par import Navigation from './Navigation/Navigation'  est notre TabNavigator à présent.

Placez-vous sur votre application et admirez le rendu :

TabNavigator sur iOS (à gauche) et sur Android (à droite)
TabNavigator sur iOS (à gauche) et sur Android (à droite)

Wow, ce n'est pas hyper joli, mais notre barre d'onglets est bien là ! :ninja: On aura l'occasion d'améliorer ce rendu très... basique.

Si vous regardez bien, on a de nouveau le problème de la barre de statut qui empiète sur notre vue, il faudra corriger ce point. 

Combinez les navigations

Je ne sais pas si vous avez testé un peu notre application, mais, depuis que l'on a mis le TabNavigator, elle ne fonctionne plus trop. Si vous faites une recherche et cliquez sur un film... rien ne se passe. :'(

À votre avis, pourquoi ? Si vous réfléchissez bien, quel composant nous a permis jusque là de passer d'une vue à l'autre ? 

C'est le StackNavigatorOn a créé un StackNavigator SearchStackNavigator spécialement pour cela, et, si vous regardez la définition de notre TabNavigator, on ne l'utilise pas. Cela explique pourquoi on ne peut plus naviguer sur l'écran de recherche.

Tout à l'heure, je vous ai un peu menti. Quand je vous disais qu'à un onglet correspond une vue, ce n'est pas tout à fait vrai. On peut aussi ajouter une navigation dans un onglet et faire ce que l'on appelle une combinaison de navigation.

C'est exactement ce dont on a besoin, intégrer notre StackNavigator dans notre TabNavigator. Je vous montre, mais c'est très simple :

// Navigation/Navigation.js
import { createStackNavigator, createAppContainer, createBottomTabNavigator } from 'react-navigation'
import Search from '../Components/Search'
import FilmDetail from '../Components/FilmDetail'
import Favorites from '../Components/Favorites'
const SearchStackNavigator = createStackNavigator({
Search: {
screen: Search,
navigationOptions: {
title: 'Rechercher'
}
},
FilmDetail: {
screen: FilmDetail
}
})
const MoviesTabNavigator = createBottomTabNavigator({
Search: {
screen: SearchStackNavigator
},
Favorites: {
screen: Favorites
}
})
export default createAppContainer(MoviesTabNavigator)

Allez-y, testez votre application. C'est mieux, non ? :) On accède de nouveau à l'écran de détail d'un film. Et regardez bien la barre de statut :

Combinaison de navigation : StackNavigator dans un TabNavigator
Combinaison de navigation : StackNavigator dans un TabNavigator

Le StackNavigator la gère toujours, elle n'empiète plus sur notre vue. Un problème à résoudre en moins.

Customisez les onglets

Actuellement, nos onglets affichent des textes, basés sur le paramètre  title  de l'option  navigationOptions  ou, par défaut, sur le nom du screen. Je vais vous montrer comment customiser nos onglets en affichant une icône à la place. On va également modifier les couleurs parce que bon, le bleu sur Android, là, je crois que tout le monde aimerait s'en passer. :D
Pour l'onglet favoris, on va réutiliser l'image que l'on a déjà, celle avec le 🖤(ic_favorite.png). Pour celle de l'onglet recherche, je vous l'ai préparée :

Image onglet recherche
Image onglet recherche

Récupérez cette image, placez-la dans le dossier /Images et renommez-la en ic_search.png.

Vous avez tous les éléments. Voici ce que l'on va appliquer sur notre TabNavigator :

  • Suppression des titres des onglets

  • Affichage des images dans les onglets

  • Modification des couleurs de la barre d'onglets

Pour faire ces changements, je me suis basé entièrement sur la documentation offerte par React Navigation, si jamais vous aviez envie d'essayer par vous-même. ;)

// Navigation/Navigation.js
import React from 'react' // N'oubliez pas l'import de React ici. On en a besoin pour rendre nos components React Native Image !
import { StyleSheet, Image } from 'react-native';
import { createStackNavigator, createAppContainer, createBottomTabNavigator } from 'react-navigation'
import Search from '../Components/Search'
import FilmDetail from '../Components/FilmDetail'
import Favorites from '../Components/Favorites'
const SearchStackNavigator = createStackNavigator({
Search: {
screen: Search,
navigationOptions: {
title: 'Rechercher'
}
},
FilmDetail: {
screen: FilmDetail
}
})
const MoviesTabNavigator = createBottomTabNavigator(
{
Search: {
screen: SearchStackNavigator,
navigationOptions: {
tabBarIcon: () => { // On définit le rendu de nos icônes par les images récemment ajoutés au projet
return <Image
source={require('../Images/ic_search.png')}
style={styles.icon}/> // On applique un style pour les redimensionner comme il faut
}
}
},
Favorites: {
screen: Favorites,
navigationOptions: {
tabBarIcon: () => {
return <Image
source={require('../Images/ic_favorite.png')}
style={styles.icon}/>
}
}
}
},
{
tabBarOptions: {
activeBackgroundColor: '#DDDDDD', // Couleur d'arrière-plan de l'onglet sélectionné
inactiveBackgroundColor: '#FFFFFF', // Couleur d'arrière-plan des onglets non sélectionnés
showLabel: false, // On masque les titres
showIcon: true // On informe le TabNavigator qu'on souhaite afficher les icônes définis
}
}
)
const styles = StyleSheet.create({
icon: {
width: 30,
height: 30
}
})
export default createAppContainer(MoviesTabNavigator)

Côté rendu, cela nous donne :

TabNavigator customisé
TabNavigator customisé

Magnifique. :D Que dire de plus ?

Tout ce que l'on vient de voir ici ne résulte pas vraiment de développement, mais plutôt de configuration : comment utiliser un composant d'une librairie externe, comment le customiser, etc. Pour rappel, il existe un 3e type de navigation avec la librairie React Navigation, en plus des StackNavigators et TabNavigators. Il s'agit du DrawerNavigator, aussi appelé menu hamburger. Si cela vous intéresse, vous pouvez regarder la documentation

Maintenant que notre navigation est en place, que vous savez utiliser et customiser un TabNavigator, vous avez tous les éléments pour créer la vue Favoris.

Préparez le terrain

Nous allons afficher, dans l'onglet favoris, les films favoris de l'utilisateur. Les films seront affichés dans une liste de films équivalente à celle utilisée dans la vue recherche. Au clic sur un film, on accède à son détail. Voici le rendu final que l'on souhaite atteindre :

Vue Favoris et fonctionnement sur iOS (en haut) et Android (en bas)
Vue Favoris et fonctionnement sur iOS (en haut) et Android (en bas)

C'est la même vue que la vue recherche, à la différence ici que l'on affiche les films favoris. Vous allez peut-être me dire :

Facile, ici, j'ai juste à prendre le component Search, le copier-coller dans mon nouveau component Favorites ...

Je pense que vous connaissez déjà mon point de vue sur un éventuel copié-collé ici. :diable: React Native, via son concept de component, nous offre la possibilité de découper notre application en pièce réutilisable. On va donc toujours chercher à exploiter cette fonctionnalité.

La partie que l'on souhaite utiliser, dans la vue recherche et la vue Favoris, est la liste de films. On va donc commencer par "sortir" notre liste de films pour en créer un component custom à part entière. Une fois notre liste de films sous forme de component custom, on pourra l'utiliser dans la vue recherche et dans la vue Favoris.

Component custom liste de films

On va appeler ce component FilmList. Avant de créer ce component, posez-vous la question : 

Quelle sera la fonction de ce component ? 

Ce à quoi nous pouvons répondre ici : afficher une liste de films et naviguer vers le détail d'un film. C'est tout ! Cela paraît évident, mais c'est très important de définir le périmètre de vos components afin de les rendre au maximum réutilisables

Je veux dire par là que ce n'est pas à notre liste de films de récupérer les films depuis l'API ou du store Redux. Le component FilmList va recevoir des films de par ses props, les afficher et, au clic sur un film, naviguer vers son détail

Pour l'instant, on va se concentrer sur la vue recherche, qui fonctionne. Nous allons "sortir" la liste de films du rendu du component Search, créer et utiliser le component FilmList. Puis, on s'assurera que tout fonctionne de nouveau.

Je l'ai fait pour vous, étant donné qu'il n'y a pas de concepts nouveaux, mais je vous invite vraiment à essayer par vous-même, cette fois encore, avant de découvrir la solution :

// Components/Search.js
import React from 'react'
import { StyleSheet, View, TextInput, Button, Text, FlatList, ActivityIndicator } from 'react-native'
import FilmItem from './FilmItem'
import FilmList from './FilmList'
import { getFilmsFromApiWithSearchedText } from '../API/TMDBApi'
class Search extends React.Component {
constructor(props) {
super(props)
this.searchedText = ""
this.page = 0
this.totalPages = 0
this.state = {
films: [],
isLoading: false
}
}
_loadFilms() {
if (this.searchedText.length > 0) {
this.setState({ isLoading: true })
getFilmsFromApiWithSearchedText(this.searchedText, this.page+1).then(data => {
this.page = data.page
this.totalPages = data.total_pages
this.setState({
films: [ ...this.state.films, ...data.results ],
isLoading: false
})
})
}
}
_searchTextInputChanged(text) {
this.searchedText = text
}
_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: [],
}, () => {
this._loadFilms()
})
}
_displayLoading() {
if (this.state.isLoading) {
return (
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
</View>
)
}
}
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Rechercher' onPress={() => this._searchFilms()}/>
<FilmList
films={this.state.films} // C'est bien le component Search qui récupère les films depuis l'API et on les transmet ici pour que le component FilmList les affiche
navigation={this.props.navigation} // Ici on transmet les informations de navigation pour permettre au component FilmList de naviguer vers le détail d'un film
loadFilms={this._loadFilms} // _loadFilm charge les films suivants, ça concerne l'API, le component FilmList va juste appeler cette méthode quand l'utilisateur aura parcouru tous les films et c'est le component Search qui lui fournira les films suivants
page={this.page}
totalPages={this.totalPages} // les infos page et totalPages vont être utile, côté component FilmList, pour ne pas déclencher l'évènement pour charger plus de film si on a atteint la dernière page
/>
{this._displayLoading()}
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1
},
textinput: {
marginLeft: 5,
marginRight: 5,
height: 50,
borderColor: '#000000',
borderWidth: 1,
paddingLeft: 5
},
loading_container: {
position: 'absolute',
left: 0,
right: 0,
top: 100,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
})
export default Search
// Components/FilmList.js
import React from 'react'
import { StyleSheet, FlatList } from 'react-native'
import FilmItem from './FilmItem'
import { connect } from 'react-redux'
class FilmList extends React.Component {
constructor(props) {
super(props)
this.state = {
films: []
}
}
_displayDetailForFilm = (idFilm) => {
console.log("Display film " + idFilm)
// On a récupéré les informations de la navigation, on peut afficher le détail du film
this.props.navigation.navigate('FilmDetail', {idFilm: idFilm})
}
render() {
return (
<FlatList
style={styles.list}
data={this.props.films}
extraData={this.props.favoritesFilm}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => (
<FilmItem
film={item}
isFilmFavorite={(this.props.favoritesFilm.findIndex(film => film.id === item.id) !== -1) ? true : false}
displayDetailForFilm={this._displayDetailForFilm}
/>
)}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.props.page < this.props.totalPages) {
// On appelle la méthode loadFilm du component Search pour charger plus de films
this.props.loadFilms()
}
}}
/>
)
}
}
const styles = StyleSheet.create({
list: {
flex: 1
}
})
const mapStateToProps = state => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmList)

J'ai rajouté des commentaires pour expliquer les démarches. Normalement, pas de surprises ici.

Allez-y, testez sur device, jouez un peu avec l'application, testez toutes les fonctionnalités de notre liste de films et...

Pourtant, cela marchait très bien avant. :waw: On n'a pas changé grand-chose ici pourtant. La seule chose que l'on a changée est, dans le component Search, de faire passer la méthode  loadFilms  au component FilmList :

// Components/Search.js
<FilmList
//...
loadFilms={this._loadFilms}
/>

Puis, toujours depuis le component FilmList, on a exécuté  loadFilms  quand l'utilisateur a scrollé toute la liste de films :

// Components/FilmList.js
onEndReached={() => {
if (this.props.page < this.props.totalPages) {
this.props.loadFilms()
}
}}

Enfin, côté component Search, on exécute la méthode  loadFilms  : 

// Components/Search.js
_loadFilms() {
if (this.searchedText.length > 0) { // BOOM ici !
//...
}
}

Ne cherchez pas plus, vous ne pouvez pas le deviner. C'est un problème de data binding.

On arrive à la fin de ce chapitre et...

Attends, là, tu arrêtes ce chapitre en plein milieu d'une erreur ? Et sans nous expliquer ce qu'est le data binding ?

Oui, c'est moi qui décide. :lol: Pas d'inquiétudes, on résout cette anomalie et on parle du data binding dans le prochain chapitre. Au début, je voulais aborder ce sujet rapidement, résoudre l'anomalie et passer à la suite, mais c'est un gros morceau. L'anomalie n'est pas si simple, vous verrez.

De plus, vous allez forcément être confronté à cette anomalie tôt ou tard dans vos applications. Je vais tout faire pour vous y préparer et que vous compreniez ce qu'il se passe.

Dans ce chapitre, on aura vu comment créer une navigation avancée avec le composant TabNavigator de la librairie React Navigation. On a également vu comment combiner les navigations entre elles. D'ailleurs, rien ne vous empêche d'aller encore plus loin. Vous pouvez très bien imaginer un menu hamburger DrawerNavigator qui, dans une entrée, affiche une vue avec un TabNavigator, qui lui-même affiche un StackNavigator. Bref, tout est possible, tant que l'utilisateur ne s'y perd pas. ^^ Et si jamais la librairie React Navigation ne fait pas ce que vous voulez, vous pouvez toujours créer votre propre navigation. 

C'est le moment de faire une pause. Même si je considère que Redux est la partie la plus difficile à appréhender dans ce cours, le concept de data binding n'est pas si simple. On va voir beaucoup de schémas, de notions différentes et il ne faudra pas tout mélanger. Quand vous vous sentez prêt, lancez-vous et "bindons" ! :pirate:

Exemple de certificat de réussite
Exemple de certificat de réussite