• 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

Ajoutez des fonctionnalités sur un component

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

Dans ce chapitre, nous allons revoir des notions et concepts appris précédemment, afin de vous perfectionner dans leur utilisation. On ne verra donc rien de nouveau. Rassurez-vous, même si vous n'êtes pas bien réveillé, ce chapitre devrait bien se passer. :D 

Vous allez voir qu'avec tout ce que l'on a déjà appris, on est en mesure de créer des fonctionnalités vraiment sympas et très rapidement.

Au programme, je vous propose d'ajouter 3 fonctionnalités dans notre component Search :

  • ajouter un composant de chargement pour faire patienter l'utilisateur pendant la récupération des films de l'API ;

  • valider la recherche avec le clavier, en plus de la validation avec le bouton "Rechercher" ;

  • rendre notre liste de films plus "lisible", en afficher le poster du film, parce que, oui, on a toujours notre rectangle gris. :o Alors, commençons par cela.

Récupérez le poster d'un film

Si vous avez regardé attentivement le JSON renvoyé par l'API, très attentivement, :lol: vous avez dû remarquer un champ ressemblant à ceci :

"poster_path": "/dHhyFNvAGnc1vtXWXWnnEfG1dLO.jpg"

On a donc le nom de l'image, mais cela s'arrête là. Il nous manque tout le début de l'URL. On va donc créer une fonction pour construire l'URL d'une image et on va le faire dans notre fichier API/TMDBApi.js : 

// API/TMDBApi.js
export function getImageFromApi (name) {
return 'https://image.tmdb.org/t/p/w300' + name
}

Ici, je vous avoue que vous n'auriez pas pu le deviner, en tout cas pour l'URL complète. L'API TMDB nous permet également de récupérer une taille particulière pour notre image. Ici, j'ai demandé une image avec une largeur de 300.

À présent, il ne nous reste plus qu'à ajouter notre image dans le component Image des items FilmItem. N'oubliez pas l'import de la fonction  getImageFromApi  : 

// Components/FilmItem.js
import { getImageFromApi } from '../API/TMDBApi'
// ...
<Image
style={styles.image}
source={{uri: getImageFromApi(film.poster_path)}}
/>

Rien de très compliqué ici : on définit la source de notre image avec l'URL construite par notre fonction  getImageFromApi  . 

Jetez un œil côté application, lancez une recherche :

Récupération des posters des films de l'API TMDB
Récupération des posters des films de l'API TMDB

C'est quand même plus joli comme ça. :soleil: Vous pouvez également enlever le style backgroundColor gris que l'on a mis sur l'image, il ne sert plus à présent.

Validation de la recherche avec le clavier

On continue avec une fonctionnalité très facile à mettre en place et qui offre une bien meilleure expérience utilisateur. Actuellement, pour rechercher des films, on tape du texte dans notre TextInput et on clique sur le Button "Rechercher". Certains utilisateurs, comme moi, :) ont l'habitude d'appuyer sur le bouton "Envoyer" du clavier pour valider un TextInput. C'est très facile à faire, mais cela a le mérite de vous faire rechercher dans la documentation React Native.

Je vous invite donc à consulter les props possibles sur un TextInput et à identifier la prop qui semble correspondre au mieux à notre fonctionnalité. 

Alors, vous avez trouvé ? 

On va utiliser la prop onSubmitEditing

Callback that is called when the text input's submit button is pressed. Invalid if multiline={true} is specified.

On n'est pas dans le cas d'un TextInput multilignes ; c'est exactement ce que l'on souhaite mettre en place. On ajoute donc cette prop à notre TextInput et, vu que l'on a déjà une fonction pour lancer la recherche, on va la réutiliser :

<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._loadFilms()}
/>

Voilà comment, en quelques minutes, améliorer l'expérience utilisateur de votre application. On continue. :pirate:

Affichage d'un chargement lors de la recherche

On a de la chance, l'API TMDB répond très rapidement aux appels qu'on lui fait. De plus, sauf si vous êtes actuellement au sommet du Mont Blanc, vous devez voir les films s'afficher instantanément. Toutefois, la connexion de votre utilisateur varie souvent, parfois elle monte très haut, parfois elle descend très bas. Dans le cas où sa connexion ralentirait, il connaîtrait une très mauvaise expérience utilisateur. Il s'agit du "bouton qui ne change rien".

Votre utilisateur va lancer une recherche en cliquant sur le bouton "Rechercher" ou "Envoyer" du clavier. Vous allez lancer l'appel API et attendre son retour. Entre l'appui sur le bouton et le retour de l'API, il ne se passe rien sur notre application. Si le retour API met 5 secondes, pendant 5 secondes, l'utilisateur attend, ne voit aucun changement et va avoir l'impression que le bouton ne fait rien. Pour pallier cette incompréhension, rien de mieux que d'afficher un chargement à l'écran.

React Native met à disposition un component React Native spécialement pour les chargements : l'ActivityIndicator. Avec ce component, je veux que vous mettiez en place ceci : 

Démonstration ActivityIndicator sur iOS et Android
Démonstration ActivityIndicator sur iOS et Android

Posez-vous la question : que souhaite-t-on faire ici exactement ?

On souhaite afficher un chargement ActivityIndicator au lancement de la recherche par-dessus notre FlatList. Puis, à la fin de l'appel API, on souhaite juste enlever le chargement.

Gestion du chargement

Première question à se poser : est-ce qu'une variable  isLoading  , pour afficher ou masquer notre chargement, a sa place dans le state ?

Oui, on veut qu'à son chargement, avec  setState  , notre component Search soit re-rendu pour afficher, ou non, le chargement.

Je vous invite donc à rajouter un booléen  isLoading  dans notre state :

// Components/Search.js
this.state = {
films: [],
isLoading: false // Par défaut à false car il n'y a pas de chargement tant qu'on ne lance pas de recherche
}

Ensuite, on va gérer le lancement du chargement et son arrêt. Si vous me suivez jusque là, on doit lancer le chargement quand on fait l'appel à l'API et l'arrêter quand l'API nous retourne les films :

// Components/Search.js
_loadFilms() {
if (this.searchedText.length > 0) {
this.setState({ isLoading: true }) // Lancement du chargement
getFilmsFromApiWithSearchedText(this.searchedText).then(data => {
this.setState({
films: data.results,
isLoading: false // Arrêt du chargement
})
})
}
}

On peut déjà vérifier que notre booléen gère correctement le chargement. Ajoutez un log dans le render du component Search :

// Components/Search.js
render() {
console.log(this.state.isLoading)
return ( ... )
}

Sur l'application, rechargez la vue, saisissez du texte dans le TextInput et cliquez sur le bouton "Rechercher".

Vous devriez voir, dans vos logs, quelque chose comme ceci :

13:49:32: false
13:49:33: true
13:49:33: false

Ça marche. Notre booléen qui gère le chargement passe bien à true pendant l'appel à l'API, puis à false quand l'API nous retourne les films. On peut continuer.

À présent, il faut  :

  • pendant le chargement, afficher notre ActivityIndicator ;

  • en dehors du chargement, enlever notre ActivityIndicator.

Gestion de l'ActivityIndicator

On va créer une fonction qui va gérer tout cela pour nous et, vous allez le voir, c'est très simple :

// Components/Search.js
import { ..., ActivityIndicator } from 'react-native'
class Search extends React.Component {
// ...
_displayLoading() {
if (this.state.isLoading) {
return (
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
{/* Le component ActivityIndicator possède une propriété size pour définir la taille du visuel de chargement : small ou large. Par défaut size vaut small, on met donc large pour que le chargement soit bien visible */}
</View>
)
}
}
// ...
}
// Components/Search.js
const styles = StyleSheet.create({
...
loading_container: {
position: 'absolute',
left: 0,
right: 0,
top: 100,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
})

Pourquoi crées-tu une View dans laquelle tu mets ton ActivityIndicator, au lieu de créer juste un ActivityIndicator ?

Je souhaite centrer mon ActivityIndicator dans l'espace occupé par ma FlatList normalement. Je commence donc par créer une View qui va occuper tout l'espace dans mon écran, ou plutôt, quasiment tout l'espace :  top: 100, left: 0, right: 0, bottom: 0 . Ensuite, rappelez-vous, lorsque l'on souhaite définir l'alignement de nos components, il faut appliquer les styles d'alignement sur le component parent. Ici, je souhaite aligner mon ActivityIndicator, j'applique donc les styles d'alignement sur son component parent, ma View. Par défaut, le style  flexDirection  vaut  'column' , le  justifyContent: 'center'  centre donc mon component sur l'axe vertical Y, et le  alignItems: 'center'  centre mon component sur l'axe horizontal X. Même si vous aviez sûrement compris le fonctionnement ici, cela ne fait pas de mal de faire une piqûre de rappel sur les styles. :)

À présent, on va modifier le rendu de notre component pour appeler cette fonction et afficher, ou non, le chargement :

// Components/Search.js
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._loadFilms()}
/>
<Button title='Rechercher' onPress={() => this._loadFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
/>
{this._displayLoading()}
</View>
)
}

Allez-y, essayez ! Placez-vous sur votre device et lancez une recherche. Vous devriez voir apparaître un chargement qui s'affiche, puis se masque lorsque les films sont récupérés.

Tout dans le render

Dans mon exemple précédent, j'ai créé une fonction  _displayLoading()  pour afficher, ou non, le chargement. J'ai en quelque sorte externalisé le rendu du chargement. Vous vous êtes peut-être demandé pourquoi ne pas tout laisser dans le render de notre component. C'est tout à fait possible, on peut très bien faire : 

// Components/Search.js
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._loadFilms()}
/>
<Button title='Rechercher' onPress={() => this._loadFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
/>
{ this.state.isLoading ?
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
</View>
: null
}
</View>
)
}

Honnêtement, cela se défend très bien comme solution. On garde toute la logique du  render  de notre component, dans son  render  .

Pourtant, en React Native, on utilise assez peu cette solution. Le problème, quand vous déclarez un test directement dans le  render  du component, est que vous êtes obligé de passer par une condition en ligne, vous savez cette syntaxe  test ? vrai : faux . Ici, c'est n'est pas très propre, car, dans le cas où le chargement est fini, on est obligé de retourner null (ligne 15). De plus, avec les conditions en ligne, si vous devez faire deux actions différentes, cela va (  if else  ). Si c'est plus complexe (if elseif if else etc. ), cela devient vite difficile à mettre en place. 

J'avais pas mal de choses à vous dire sur le rendu "conditionnel" dans vos components. C'est un point important auquel vous serez confronté lors de la réalisation d'applications plus fonctionnelles et plus poussées.

Notre chargement est en place, parfait.

Et... c'est terminé. Eh oui, on a déjà mis en place les 3 fonctionnalités que l'on voulait. C'était rapide, n'est-ce pas ? ;)

Vous voyez qu'avec tout ce que l'on a déjà appris, on est capable de créer des fonctionnalités vraiment sympas, simplement et très rapidement

Dans le prochain chapitre, nous allons ajouter une autre fonctionnalité à notre component Search. Cette fonctionnalité va mettre en évidence une spécificité de  setState . Car oui, setState  nous cache des choses. :ninja:

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