• 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

Réalisez des développements spécifiques

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

Le développement spécifique est l'écriture de code propre à une plateforme : iOS / Android. Lorsque l'on développe sur React Native et sur les frameworks cross-platforms, on développe son application une seule fois et, par conséquent, les comportements sont les mêmes sur les deux plateformes. On souhaite parfois avoir un comportement différent en fonction de la plateforme. Sur iOS, vous voudrez avoir le bouton en haut à droite, alors que, sur Android, vous le voudrez en bas à gauche, etc. 

Le développement spécifique peut aussi concerner le cas où vous souhaitez forcer un comportement à être le même sur iOS et Android. Prenons l'exemple des boutons. Par défaut, ils s'affichent différemment sur iOS et Android. Peut-être que, dans une future application, vous voudrez que vos boutons soient identiques. Dans ce cas, vous allez réaliser un développement spécifique pour forcer vos boutons à être les mêmes sur iOS et Android

Le développement spécifique va à l'encontre de la philosophie du développement cross-platform, puisque l'on va "casser" la notion de code unique pour les deux plateformes et créer deux codes différents : on duplique le code. Mais cela reste une fonctionnalité très importante et très utile. Vos applications doivent séduire les utilisateurs iOS et Android et ils n'ont pas les mêmes attentes, les mêmes habitudes. Le développement spécifique permet de leur offrir une meilleure expérience utilisateur (UX), alors, pourquoi s'en passer ? :) 

Dans ce chapitre, je vais vous apprendre comment réaliser des développements spécifiques. On verra comment ajuster un simple style jusqu'à différencier des components entiers.

Module Platform

React Native dispose d'un module Platform permettant de récupérer des informations sur la plateforme utilisée par l'utilisateur. Avec ce module, vous pourrez savoir si l'utilisateur est sur iOS ou Android et quelle version il utilise. 

Style spécifique

On va commencer par le plus simple, à savoir créer un style spécifique.  Deux solutions :

  • soit vous utilisez la fonction  Platform.select  qui renvoie la plateforme sous forme de clé ;

  • soit vous testez directement la valeur de l'OS.

Je vous mets les deux solutions :

// Components/Test.js
import { ..., Platform } from 'react-native'
// Soit on utilise la fonction Platform.select
subview_container: {
...Platform.select({
ios: {
backgroundColor: 'red',
height: 100,
width: 50
},
android: {
backgroundColor: 'blue',
height: 50,
width: 100
}
})
}
// Soit on teste la valeur de l'OS
subview_container: {
backgroundColor: Platform.OS === 'ios' ? 'red' : 'blue',
height: Platform.OS === 'ios' ? 100 : 50,
width: Platform.OS === 'ios' ? 50 : 100
}

Ici, je cherche à afficher :

  • une vue rouge de 100 pixels de hauteur par 50 pixels de largeur sur iOS ;

  • une vue bleue de 50 pixels de hauteur et 100 pixels de largeur sur Android.

Ce qui nous donne, côté application :

Développement spécifique appliqué aux styles
Développement spécifique appliqué aux styles

Ça marche. :) J'ai bien mes deux vues affichées différemment sur iOS et Android. Bien sûr, mon exemple n'a pas vraiment d'intérêt, mais, au moins, vous connaissez la démarche.

Le développement spécifique sur les styles sert souvent à ajuster vos vues, en appliquant des margins, paddings... différents. À vrai dire, je m'en sers très peu, mais, dans certains cas, cela peut être utile.

Code spécifique, component spécifique...

Ce que j'ai appliqué aux styles, vous pouvez le faire pour toute votre application. Vous pouvez très bien avoir un rendu spécifique, comme ici :

render() {
return (
{ Platform.OS === 'ios' ? <Text>iOS</Text> : <Text>Android</Text> }
)
}

Voire un component entier spécifique :(exemple repris de la documentation) 

const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid')
})()
<Component />

Il n'y a aucune limite, si ce n'est le module Platform lui-même. Il ne permet pas de différencier beaucoup de cas. Vous pouvez savoir si l'utilisateur est sur iOS, la version qu'il utilise et... c'est tout. Il y a une propriété  isPad, mais disponible uniquement sur iOS pour l'instant. On ne peut pas non plus savoir si l'utilisateur est sur iPhone 6 ou 6 Plus ni avoir des infos sur l'identifiant du device, etc. Bref, même si le type de plateforme et la version sont souvent largement suffisants, sur certaines applications, on peut vite se retrouver limité

Si votre application nécessite des développements spécifiques plus poussés, en fonction de critères plus précis, il existe la librairie react-native-device-info qui permet d'obtenir beaucoup d'informations sur le device de l'utilisateur. Je vous invite à aller y jeter un œil.

Il existe une autre option pour développer spécifiquement, sans utiliser le module Platform, où tout commence (et se termine) dans le nommage de nos fichiers Javascript.

Platform extension

Il est possible de nommer ses fichiers Javascript sous la forme : nom.plateforme.js. React Native va ensuite charger lui-même les bons fichiers en fonction de la plateforme sur laquelle est l'utilisateur.

Prenons l'exemple où vous souhaitez afficher un component différent sur iOS et Android. On a vu la solution précédente avec la fonction  const Component = Platform.select, mais il y a beaucoup plus simple.

Dans notre application, créez deux components HelloWorld.ios.js et HelloWorld.android.js. On n'allait quand même pas finir ce cours sans faire le fameux HelloWorld. :lol:

// Components/HelloWorld.ios.js
import React from 'react'
import { Text } from 'react-native'
class HelloWorld extends React.Component {
render() {
return (
<Text>Hello iOS</Text>
)
}
}
export default HelloWorld
// Components/HelloWorld.android.js
import React from 'react'
import { Text } from 'react-native'
class HelloWorld extends React.Component {
render() {
return (
<Text>Hello Android</Text>
)
}
}
export default HelloWorld

Puis, côté component Test, on va ajouter ce nouveau component :

// Components/Test.js
import HelloWorld from './HelloWorld'
render() {
return (
<View style={styles.main_container}>
<HelloWorld/>
</View>
)
}

Si vous testez l'application, vous devriez voir :

Nommage des fichiers iOS / Android
Nommage des fichiers iOS / Android

Ça fonctionne ! ;) Et, ce qui est vraiment bien avec cette règle de nommage, c'est que cela s'applique à tous types de fichiers Javascript : 

  • Components : component.ios.js / component.android.js

  • Fichier de données : data.ios.js / data.android.js

  • Fichier d'API : api.ios.js / api.android.js 

  • Etc.

Et même aux images : 

  • Les images : image.ios.png / image.android.png

Ça, c'est pratique. Plus besoin de faire vos  if | else | Platform.select  dans le code. Vous pouvez vous baser uniquement sur le nom de vos fichiers / images. 

SafeArea

J'ai hésité à vous parler de la SafeArea dans ce cours, mais je pense que l'on ne peut pas passer outre. C'est une problématique que vous allez très certainement rencontrer et, quand cela arrivera, vous saurez quoi faire. ;)

Alors, commençons par le commencement. C'est quoi, SafeArea ?

La SafeArea est une invention d'Apple depuis iOS 11. Donc déjà, cela n'a rien à voir avec Android. Lorsqu'elle est utilisée dans vos vues, elle crée un cadre dans lequel vos éléments sont assurés de ne pas être masqués par des barres et autres composants du téléphone. Dit comme cela, vous ne voyez pas trop l'intérêt, mais je vous donne un exemple.

On va afficher une seule vue dans notre application et on va choisir la vue recherche. Fini la barre d'onglets, le StackNavigator, on affiche uniquement la vue recherche :

// App.js
// ...
import Search from './Components/Search'
export default class App extends React.Component {
render() {
return (
<Provider store={Store}>
<Search/>
</Provider>
)
}
}

Ce qui nous donne, côté application :

Application avec une seule vue : Recherche
Application avec une seule vue : Recherche

Vous remarquez que le TextInput, où on saisit la recherche, est masqué. Vous allez me dire :

"Pas de soucis, on met un margin top de 20 et c'est réglé, comme au début de ce cours. T'as déjà oublié ?"

Bien vu, on va faire ça :

// Components/Search.js
render() {
return (
<View style={[styles.main_container, { marginTop: 20 }]}>
//...
</View>
)
}

Cela règle le problème :

Application avec une seule vue : Recherche et marginTop à 20
Application avec une seule vue : Recherche et marginTop à 20

Mais moi, je vous dis : vous avez testé sur iPhone X ? C'est le téléphone récemment sorti par Apple qui bouleverse les codes, avec un écran qui passe sous les composants du téléphone. Je vous montre : 

Application avec une seule vue : Recherche et marginTop à 20 sur iPhone X
Application avec une seule vue : Recherche et marginTop à 20 sur iPhone X

Ohlala, mais ça ne va pas du tout ! :waw: Le TextInput passe sous la barre de statut et, en bas de l'écran, la liste passe sous le nouvel indicateur Home(le trait noir). Ce n'est vraiment pas bon du tout, mais rassurez-vous, il y a une solution.

Oui, on la connaît, tu nous en as parlé juste avant. On peut utiliser la librairie react-native-device-info et faire un cas particulier si l'utilisateur est sur iPhone X ?

Oui, on peut faire cela, mais ce n'est pas terrible du tout. Si Apple sort un nouvel iPhone avec une autre spécificité, on est bon pour modifier à nouveau son application et rajouter un nouveau cas.

J'ai autre chose, beaucoup plus simple et plus propre. On peut utiliser le component SafeAreaView créé par la communauté et qui a été intégré dans React Native depuis la version 0.50. Le SafeAreaView va créer un cadre dans lequel votre vue va être disposée. Ce cadre est une zone de sûreté en quelque sorte. Elle va démarrer en dessous des composants du téléphone et de la barre de statut et se terminer au-dessus du nouvel indicateur :(cadre rouge)

Zone de sûreté créé par la SafeAreaView
Zone de sûreté créé par la SafeAreaView

Maintenant, si on englobe notre View avec une SafeAreaView (avec le même style  flex  à 1), on obtient : (plus besoin du marginTop)

// Components/Search.js
import { ..., SafeAreaView } from 'react-native'
render() {
return (
<SafeAreaView style={styles.main_container}>
<View style={styles.main_container}>
//...
</View>
</SafeAreaView>
)
}

Et, côté application :

SafeAreaView sur notre vue recherche
SafeAreaView sur notre vue recherche

Super, notre SafeAreaView joue son rôle et s'assure que notre vue ne passe pas sous les composants du téléphone. L'avantage est que cela reste fonctionnel sur les autres téléphones iOS : iPhone 7, 6, 5, etc. (à partir d'iOS 11) et même sur Android.

Partage spécifique

Il est temps de mettre le développement spécifique en pratique sur notre application de gestion de films. Vous pouvez laisser l'onglet Test, on s'en servira encore dans le prochain chapitre. Ah et vous pouvez remettre le component Navigation dans App.js, à la place du component Search.

Dans notre application, je veux créer une fonctionnalité pour partager les films :

  • Sur Android, je veux que l'on affiche un  FloatingActionButton, le bouton par excellence d'Android. 

    FloatingActionButton d'Android
    FloatingActionButton d'Android
  • Sur iOS, je veux que l'on affiche un bouton en haut à droite, dans la barre de navigation.

L'action reste la même, on partage un film, mais le rendu du bouton permettant l'action n'est pas identique. On va commencer par la fin, c'est-à-dire créer l'action de partage.

Lorsque l'on développe en natif, il existe des solutions natives pour permettre le partage. Vous l'avez d'ailleurs peut-être déjà vu, il s'agit d'une pop-up apparaissant en bas de l'application, avec une liste de cibles de partage : message, mail, Facebook, Twitter, etc. Cela ne vous dit rien ? :euh:

Popup de partage native sur iOS (à gauche) et Android (à droite)
Pop-up de partage native sur iOS (à gauche) et Android (à droite)

Et maintenant ?

Étant donné que cela existe en natif sur les plateformes iOS et Android, vous vous doutez bien que React Native y a pensé aussi. :)

On va utiliser l'API Share de React Native qui dispose d'une fonction toute prête  share(content, options) . On va définir le  content  avec un titre et un message qui vont correspondre, respectivement chez nous, au titre du film et à sa description : 

// Components/FilmDetail.js
import { ..., Share } from 'react-native'
//...
_shareFilm() {
const { film } = this.state
Share.share({ title: film.title, message: film.overview })
}
render() {
//...
}

Android

On va commencer par Android, en affichant notre FloatingActionButton lorsque le film est bien chargé. Il n'y a pas de component FloatingActionButton en React Native, il faut créer un bouton custom. Rappelez-vous ce que l'on a dit, les Buttons sur React Native sont peu customisables, on va donc utiliser une Touchable avec une image à l'intérieur pour réaliser ce bouton. Pour l'image, c'est celle-ci : (l'image est blanche ;))

Image de partage d'Android (blanche)
Image de partage d'Android (blanche)

Placez cette image dans le dossier /Images et nommez-la : ic_share.android.png

Pour notre bouton, voici le code que j'ai utilisé. Il ne faut pas oublier de spécifier que l'on ne souhaite ce bouton que sur Android :

// Components/FilmDetail.js
import { ..., Platform } from 'react-native'
// ...
_displayFloatingActionButton() {
const { film } = this.state
if (film != undefined && Platform.OS === 'android') { // Uniquement sur Android et lorsque le film est chargé
return (
<TouchableOpacity
style={styles.share_touchable_floatingactionbutton}
onPress={() => this._shareFilm()}>
<Image
style={styles.share_image}
source={require('../Images/ic_share.png')} />
</TouchableOpacity>
)
}
}
render() {
return (
<View style={styles.main_container}>
{this._displayLoading()}
{this._displayFilm()}
{this._displayFloatingActionButton()}
</View>
)
}
// ...
const styles = StyleSheet.create({
//...
share_touchable_floatingactionbutton: {
position: 'absolute',
width: 60,
height: 60,
right: 30,
bottom: 30,
borderRadius: 30,
backgroundColor: '#e91e63',
justifyContent: 'center',
alignItems: 'center'
},
share_image: {
width: 30,
height: 30
}
})

Côté application Android, vous devriez avoir toute la fonctionnalité de partage opérationnelle :

Partage d'un film sur Android
Partage d'un film sur Android

 Pas mal non ? En plus, en rose, on ne peut pas rater le FloatingActionButton:lol:

iOS

Il ne nous reste plus qu'à mettre en place la fonctionnalité sur iOS. Voici l'image du bouton que l'on va utiliser :

Image de partage d'iOS
Image de partage d'iOS

Récupérez cette image, placez-là dans /Images et nommez-la : ic_share.ios.png.

On a dit que l'on affichait ce bouton en haut à droite dans la barre de navigation. Pour ce faire, après quelques recherches dans la documentation de React Navigation sur les boutons headers, on peut y lire qu'il faut surcharger la fonction qui définit les options de navigation de notre vue, à savoir la fonction navigationOptions . Une fois surchargée, il faut créer la propriété  headerRight . Je vous mets un exemple inspiré de la documentation : 

class HomeScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
return {
headerRight: (
<Button
onPress={() => alert('This is a button!')}
title="Info"
color="#fff"
/>
)
}
}
}

Dans la propriété  headerRight  , on va faire la même chose que sur Android, à savoir afficher un bouton lorsque le film est chargé et ajouter une action pour partager le film. On va donc avoir besoin de  this.state.film  et de this._shareFilm()  dans la fonction  navigationOptions . 

Dit comme cela, cela a l'air plutôt simple, mais détrompez-vous, ce ne sera pas si facile. La difficulté réside dans le fait que la fonction  navigationOptions est statique et que, malgré l'utilisation d'une fonction fléchée, elle n'est pas bindée au contexte du component. Extrait de la documentation : 

The binding of  this  in  navigationOptions  is not the HomeScreen instance, so you can't call  setState  or any instance methods on it.

On ne peut donc pas accéder au contexte  this  de notre component. Impossible d'utiliser  this.state.film  et this._shareFilm() dans la fonction qui définit les options de notre navigation. De ce fait, il est impossible de partager notre film depuis un bouton dans la navigation ? Dommage. 

Si si, ne partez pas, je ne vous ai pas emmené aussi loin pour vous dire que l'on ne peut pas. :p

Toujours depuis la documentation, on apprend qu'il faut utiliser le setter des paramètres de la navigation this.props.navigation.setParams pour faire passer nos informations à la fonction  navigationOptions . C'est ce que l'on va faire.

Je vous donne la solution, mais je vous conseille de le faire par vous-même, encore une fois. Finalement, je n'ai fait que suivre la documentation de la librairie React Navigation au sujet des options de navigation.  Vous y trouverez toutes les informations nécessaires et cela reste un super entraînement de s'appuyer entièrement sur une documentation. ;) Voici le résultat de la fonctionnalité sur iOS, avec mes commentaires :

// Components/FilmDetail.js
class FilmDetail extends React.Component {
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state
// On accède à la fonction shareFilm et au film via les paramètres qu'on a ajouté à la navigation
if (params.film != undefined && Platform.OS === 'ios') {
return {
// On a besoin d'afficher une image, il faut donc passe par une Touchable une fois de plus
headerRight: <TouchableOpacity
style={styles.share_touchable_headerrightbutton}
onPress={() => params.shareFilm()}>
<Image
style={styles.share_image}
source={require('../Images/ic_share.png')} />
</TouchableOpacity>
}
}
}
constructor(props) {
//...
// Ne pas oublier de binder la fonction _shareFilm sinon, lorsqu'on va l'appeler depuis le headerRight de la navigation, this.state.film sera undefined et fera planter l'application
this._shareFilm = this._shareFilm.bind(this)
}
// Fonction pour faire passer la fonction _shareFilm et le film aux paramètres de la navigation. Ainsi on aura accès à ces données au moment de définir le headerRight
_updateNavigationParams() {
this.props.navigation.setParams({
shareFilm: this._shareFilm,
film: this.state.film
})
}
// Dès que le film est chargé, on met à jour les paramètres de la navigation (avec la fonction _updateNavigationParams) pour afficher le bouton de partage
componentDidMount() {
const favoriteFilmIndex = this.props.favoritesFilm.findIndex(item => item.id === this.props.navigation.state.params.idFilm)
if (favoriteFilmIndex !== -1) {
this.setState({
film: this.props.favoritesFilm[favoriteFilmIndex]
}, () => { this._updateNavigationParams() })
return
}
this.setState({ isLoading: true })
getFilmDetailFromApi(this.props.navigation.state.params.idFilm).then(data => {
this.setState({
film: data,
isLoading: false
}, () => { this._updateNavigationParams() })
})
}
//...
}
const styles = StyleSheet.create({
//...
share_touchable_headerrightbutton: {
marginRight: 8
}
})

Récapitulons ! Ici, on avait besoin d'utiliser la fonction  this._shareFilm()  et le film  this.state.film  dans la fonction des options de la navigation  navigationOptions . Du fait que cette fonction soit statique et non bindée au contexte de notre component, on a dû passer par la fonction  this.props.navigation.setParams  pour transmettre la fonction et le film à la fonction  navigationOptions  .

Mais que se passe-t-il ensuite ? Une fois que l'on a setté les paramètres de la navigation ?

Les options de la navigation, et leurs rendus, dépendent entièrement de la navigation et de son state, comme votre component dépend de ses props et son state. Si vous modifiez les paramètres de la navigation, vous mettez à jour le state de votre navigation :  navigation.state.params . Par conséquent, votre navigation et ses options vont se re-rendre.

C'est exactement ce qu'il se passe ici. Dès que le film est récupéré, on met à jour les paramètres de la navigation, et donc son state. La navigation détecte un changement dans son state, les options sont re-rendues. Après, on n'a plus qu'à vérifier les nouveaux paramètres de la navigation pour afficher, ou non, l'option  headerRight. La navigation de React Navigation fonctionne comme un component.

Je raconte beaucoup de choses, mais on n'a toujours pas vu ce que cela donnait, côté application :

Partage d'un film sur iOS
Partage d'un film sur iOS 

Maintenant, si on compare le rendu sur iOS au rendu sur Android, pour la même fonctionnalité :

Comparaison du rendu du partage sur iOS et Android
Comparaison du rendu du partage sur iOS et Android

D'un côté, on a un bouton en haut à droite dans la barre de navigation (iOS), de l'autre, on a un FloatingActionButton en bas à droite dans la vue (Android). Bravo ! ;) C'est ce que l'on voulait obtenir.

Nous voilà à la fin de ce chapitre consacré au développement spécifique. Même si React Native est pensé et prévu pour créer deux applications iOS et Android avec un même code, il faut parfois procéder à des ajustements, que ce soit pour corriger des anomalies, pour respecter des comportements natifs, des lignes directrices (guidelines), etc.

On a vu également comment gérer les spécificités des téléphones, avec notamment le component SafeAreaView, sacré iPhone X... :)

Dans le prochain chapitre, on va aborder le sujet des animations. Je vous montrerai comment dynamiser votre application, la rendre plus interactive. Gardez bien l'onglet "Test" que l'on a créé pour ce chapitre, on en aura encore besoin.

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