Créer des applications mobiles avec React Native

Auteur: Mohamed CHINY Durée necessaire pour le cours de Créer des applications mobiles avec React Native Niveau recommandé pour le cours de Créer des applications mobiles avec React Native Supports vidéo non disponibles pour ce cours Exercices de renforcement disponibles pour ce cours Quiz non disponibles pour ce cours

Page 19: Exercice pratique: Développement d'une calculatrice avec React Native

Toutes les pages

Application concrète: Réaliser une calculatrice en React Native

Structurer la logique centrale avec le hook useReducer

L’objectif de cet exercice consiste à consolider les acquis en React Native. Nous allons réaliser une calculatrice simplifiée (similaire à celle présente sur nos téléphones). La logique sera principalement gérée à l’aide du hook useReducer qui centralise les actions dans un dispatcher unique. Chaque touche, qu’il s’agisse d’un chiffre ou d’une opération, déclenche une action, ce qui rend l’utilisation de useReducer particulièrement pertinente pour organiser la logique de manière cohérente et centralisée.

Notre calculatrice aura cette forme:
Calculatrice réalisée en React Native
Vous aurez sans doute constaté l’absence de la touche dédiée à la virgule. Cette omission est liée au manque d’espace, mais il est tout à fait possible de l’ajouter en appliquant la même logique que celle utilisée pour les touches numériques.
Je rappelle que notre calculatrice sera dotée des touches numériques (de 0 à 9), les touches qui représentent les opérateurs (+, -, × et ÷), la touche égal (=) et la touche C qui initialise la calculatrice.

Le fait d'actionner les touches numériques va concaténer les chiffres afin de construire le nombre souhaité. Le fait d'actionner un opérateur va vider la zone d'affichage, mais avant, il doit déclencher la mémorisation du nombre précédent afin de l'inclure dans l'opération une fois la touche égal est actionnée. Au passage, l'opérateur lui même sera mémorisé. En fin, la touche C videra la zone d'affichage et supprimera les valeurs mémorisées (opérandes et opérateur).

Comme mentionné précédemment, le hook useReducer se chargera de centraliser la logique dans un seul dispatcher. En plus, on fera appel au hook useRef aussi afin de mémoriser les opérateurs et les opérandes entre les rendus de l'application.

Je propose le code complet suivant:
import { useReducer, useRef } from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";

export default function App() {
   const prev_nbr=useRef(null)
   const op=useRef(null)
   const redu=(current_nbr,action)=>{
      switch(action){
         case "+":{
            prev_nbr.current=current_nbr
            op.current="+"
            return ""
         }
         case "-":{
            prev_nbr.current=current_nbr
            op.current="-"
            return ""
         }
         case "x":{
            prev_nbr.current=current_nbr
            op.current="x"
            return ""
         }
         case "/":{
            prev_nbr.current=current_nbr
            op.current="/"
            return ""
         }
         case "C":{
            prev_nbr.current=null
            op.current=null
            return ""
         }
         case "=":{
            switch (op.current){
               case "+": return
                  parseFloat(prev_nbr.current)
                  +parseFloat(current_nbr)
               case "-":
                  return parseFloat(prev_nbr.current)
                  -parseFloat(current_nbr)
               case "x":
                  return parseFloat(prev_nbr.current)
                  *parseFloat(current_nbr)
               case "/":
                  return current_nbr!=0?parseFloat(prev_nbr.current)
                  /parseFloat(current_nbr):"Erreur"

            }
         }
         default:
            return current_nbr.toString()
            +action.toString()
      }

   }
   const [nbr,disp]=useReducer(redu,"")
   return (
      <View style={styles.container}>
         <Text style={styles.zdt}>{nbr}</Text>
         <View style={styles.row}>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(7)}>
               <Text style={styles.number}>7</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(8)}>
               <Text style={styles.number}>8</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(9)}>
               <Text style={styles.number}>9</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("+")}>
               <Text style={styles.number}>+</Text>
            </TouchableOpacity>
         </View>
         <View style={styles.row}>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(4)}>
               <Text style={styles.number}>4</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(5)}>
               <Text style={styles.number}>5</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(6)}>
               <Text style={styles.number}>6</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("-")}>
               <Text style={styles.number}>-</Text>
            </TouchableOpacity>
         </View>
         <View style={styles.row}>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(1)}>
               <Text style={styles.number}>1</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(2)}>
               <Text style={styles.number}>2</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(3)}>
               <Text style={styles.number}>3</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("x")}>
               <Text style={styles.number}>×</Text>
            </TouchableOpacity>
         </View>
         <View style={styles.row}>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("C")}>
               <Text style={styles.number}>C</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp(0)}>
               <Text style={styles.number}>0</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("=")}>
               <Text style={styles.number}>=</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.btn}
            onPress={()=>disp("/")}>
               <Text style={styles.number}>÷</Text>
            </TouchableOpacity>
         </View>
      </View>
   );
}

const styles = StyleSheet.create({
   container: {
      flex: 1,
      backgroundColor: '#fff',
      alignItems: 'center',
      justifyContent: 'center',
      gap:20
   },
   zdt:{
      borderWidth:1,
      width:300,
      padding:10,
      borderRadius:10,
      fontSize:20,
      textAlign:"right"
   },
   row:{
      flexDirection:"row",
      gap:20
   },
   btn:{
      backgroundColor:"#CCC",
      padding:10,
      width:60,
      borderRadius:10
   },
   number:{
      textAlign:"center",
      fontSize:20
   }
})

Explication du code pas à pas

On commence par importer les modules requis pour l'application:
import { useReducer, useRef } from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";
Comme expliqué précédemment, l'importation explicite n'est pas toujours demandée car votre IDE se charche de l'exécuter une fois il détecte l'invocation d'une fonction (ou composant) lors de la saisie du code.
Dans la partie logique du composant principal App, on crée deux objets prev_nbr et op (à l'aide du hook useRef) qui servent respectivement à mémoriser l'opérande précédent et l'opération voulue:
const prev_nbr=useRef(null)
const op=useRef(null)
On crée ensuite notre state nbr à l'aide du hook useReducer. En même temps, on nomme le dispatcher disp et le reducer redu:
const [nbr,disp]=useReducer(redu,"")
La valeur initiale du state est une chaîne vide, car le fait d'actionner une touche déclenchera la concaténation des chiffres. Donc, il est préférable que le type initiale soit une chaîe de caractères.

Avant l'invocation de useReducer, on doit définir le reducer lui même:
const redu=(current_nbr,action)=>{
   ...
}
Comme vu dans la leçon consacrée à useReducer, le reducer accepte deux arguments: la valeur courante du state (dans ce cas current_nbr) et l'action qui sera spécifiée par le dispatcher.
N'oubliez pas que le reduder doit retourner la nouvelle valeur du state, ce qui déclenchera automatiquement le re-rendering du composant.
Comme c'est souvent le cas dans le reducer, on utilise la structure switch ... case pour couvrir les cas possibles d'actions.
switch(action){
   case "+":{
      prev_nbr.current=current_nbr
      op.current="+"
      return ""
   }
   ...
}
Dans cet extrait du code, si l'action vaut "+" (ce qui signifie que l'on a actionné la touche d'addition), alors on doit d'abord enregistrer l'opérande courant (current_nbr) dans l'objet prev_nbr (via prev_nbr.current=current_nbr), on enregistre aussi l'opération (op.current="+") puis en retourne une chaine vide, ce qui signifie que le state nbr reçoit une chaine vide (vidant ainsi la zone d'affichage pour accueillir soit le deuxième opérande, soit le résultat de l'opération).

On procède de la même façon pour les autres opérateurs (-, × et ÷).

Si l'action vaut "C", alors on initialiser notre calculatrice en vidant les valeurs mémorisées dans les objets prev_nbr et op. On retourne également une chaine vide (qui fait office de la nouvelle valeur du state nbr) pour vider l'affichage.
case "C":{
   prev_nbr.current=null
   op.current=null
   return ""
}
Si l'action vaut "=", alors on programme une nouvelle structure switch ... case pour décider quelle opération exécuter en évaluant la valeur mémorisée dans l'objet op:
case "=":{
   switch (op.current){
      case "+":
         return parseFloat(prev_nbr.current)
         +parseFloat(current_nbr)
      ...
   }
}
Dans ce cas, le reducer retourne le résultat de l'opération entre la valeur mémorisée dans l'objet prev_nbr et le nombre courant actuellement affiché current_nbr.
N'oubliez pas d'évaluer les opérandes en tant que nombre à l'aide de la fonction parseFloat (ou parseInt si vous préférez les entiers). Sinon l'addition sera prise une concaténation par l'interpréteur.

On procède de la même manière pour les autres cas.

Le seul cas qui reste à gérer par le dispatcher est le fait d'actionner une touche numérique. Dans ce cas, on procède simplement à la concaténation des numéros pour construire la nouvelle valeur du state nbr. Ce cas a été gérée comme action par défaut:
default: return current_nbr.toString()+action.toString()
Dans ce cas, n'oubliez par de convertir la valeur courante current_nbr et l'action en chaîne de caractères afin que l'opérateur + soit évalué comme opérateur de concaténation et non pas d'addition.
Quant aux composants, on a fait en sorte d'aligner chaque rangée de 4 touches dans une seule ligne en appliquant le style flex-direction:row dans chacun des composants View prévus à cet effet:
<View style={styles.row}>
   <TouchableOpacity style={styles.btn}
   onPress={()=>disp(7)}>
      <Text style={styles.number}>7</Text>
   </TouchableOpacity>
   <TouchableOpacity style={styles.btn}
   onPress={()=>disp(8)}>
      <Text style={styles.number}>8</Text>
   </TouchableOpacity>
   <TouchableOpacity style={styles.btn}
   onPress={()=>disp(9)}>
      <Text style={styles.number}>9</Text>
   </TouchableOpacity>
   <TouchableOpacity style={styles.btn}
   onPress={()=>disp("+")}>
      <Text style={styles.number}>+</Text>
   </TouchableOpacity>
</View>
Enfin, il suffit d'appeler le dispatcher disp en lui passant l'action souhaitée (numéro, opération, calcul du résultat ou initialisation).
Rappelons que l’appel au dispatcher se fait via une fonction anonyme, puisqu’il nécessite un argument. Si vous l’invoquez directement, il sera exécuté dès le chargement du composant au lieu d’attendre l’action sur un bouton.