2.1 - Atelier


Initialisation

npx create-expo-app todo

cd todo

npx expo start

Lancer l'application dans Expo Go.

Structure du projet

├── App.js    # Point d'entrée du code
├── app.json  # Metadonnées du projet: nom, version, configurations...
├── assets    # Ressources statiques: images, sons, pdf, etc.
│   ├── icon.png
│   └── splash.png
├── babel.config.js  # Configuration de build
├── .expo            # Fichiers locaux de l'environnement de build Expo
├── .gitignore
├── package.json     # Liste des dépendances JS
└── package-lock.json

App.js

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
      {/*
        Qu'est-ce que cette balise?
        https://docs.expo.dev/versions/latest
        Tester la prop 'hidden'
      */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

UI

Installer Safe Areas Context

npx expo install react-native-safe-area-context

# npx expo install permet de s'assurer d'avoir la version de la librairie compatible avec la version React Native du projet

Autoriser toutes les orientations en modifiant app.json

{
  "expo": {
    "orientation": "default",
    ...
  }
}

Définir l'interface avec JSX et les components React Native.

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, ScrollView, Text, TextInput, Switch, Button, TouchableHighlight } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';

export default function App() {
  return (
    <>
        <StatusBar style="auto" />

        <SafeAreaProvider>
            <SafeAreaView style={styles.container}>

              <Text style={{ fontSize: 28, fontWeight: 'bold', textAlign: 'center' }}>Todoer</Text>

              <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>

                <TextInput 
                  placeholder='New task'
                  style={[styles.input, { height: 48, flexGrow: 1 }]} 
                /> 

                <Switch />

              </View>

              <TextInput 
                placeholder='Optional description'
                style={[styles.input, { width: '100%', height: 96, verticalAlign: 'top' }]} 
                multiline={ true } 
              />

              <View style={{ flexDirection: 'row', justifyContent: 'center' }}>
              {/* Envelopper dans une View pour eviter une largeur de 100% */}

                  <Button 
                    title='Add' 
                    color='green' 
                  />

              </View>

              {/* Pas la facon ideale pour afficher une liste, simplification pour la demonstration */}
              <ScrollView style={{ flexGrow: 1 }}>
              {
                  [...Array(10).keys()].map((item, index, array) => {
                    return (
                        <TouchableHighlight key={item} onPress={ () => {} }>
                            <View 
                              style={{ 
                                flexDirection: 'row',
                                paddingVertical: 16, 
                                paddingHorizontal: 8,
                                backgroundColor: 'white', /* requis pour highlight */
                                borderBottomWidth: (index != array.length -1) ? 1 : 0,
                                borderBottomColor: 'lightgray'
                              }}
                            >
                                <Ionicons name="checkmark-circle" size={22} color={ index % 5 == 0 ? 'green' : 'lightgray' } />

                                <View style={{ flex: 1, flexDirection: 'column' }}>
                                    <Text style={[styles.text, { fontWeight: 'bold' }]}>TODO { item }</Text>
                                    { 
                                      index > 0 &&
                                      <Text style={ styles.text }>{ 'Description '.repeat(index) }{ item }</Text>
                                    }
                                </View>
                            </View>
                        </TouchableHighlight>
                    )
                })
              }
              </ScrollView>

            </SafeAreaView>
        </SafeAreaProvider>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    gap: 16,
    justifyContent: 'center',
    padding: 16,
  },
  input: {
    borderWidth: 1,
    borderColor: 'lightgray',
    padding: 8,
  },
  text: {
    fontSize: 18,
  },
});

📚 Flexbox

📚 React Native Components

📚 UI Libraries

Components

Éventuellement, on veut encapsuler les responsabilités des différents blocs de l'interface en utilisants des components. Un component est une balise personnalisée qu'on peut réutiliser au besoin.

  • Attention on peut atteindre un point de rupture où la séparation en component augmente la complexité de l'application, surtout si on doit communiquer entre les components.

Par exemple, pour gérer le titre et les zones de saisies

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, ScrollView, Text, TextInput, Switch, Button, TouchableHighlight } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';

function Title({ text }) {
  return <Text style={{ fontSize: 28, fontWeight: 'bold', textAlign: 'center' }}>{ text }</Text>
}

function Input(props) {
    const {style, ...otherProps} = props;

    return <TextInput {...otherProps} style={[styles.input, style]} />
}

export default function App() {
  return (
    <>
        <StatusBar style="auto" />

        <SafeAreaProvider>
            <SafeAreaView style={styles.container}>

            <Title text="Todoer" />

              <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>

                <Input 
                  placeholder='New task'
                  style={{ height: 48, flexGrow: 1 }} 
                /> 

                <Switch />

              </View>

              <Input 
                placeholder='Optional description'
                style={{ width: '100%', height: 96, verticalAlign: 'top' }} 
                multiline={ true } 
              />

            ...

            </SafeAreaView>
        </SafeAreaProvider>
    </>
  );
}


Pour faciliter l'organisation et la navigation dans le projet, on peut exploiter les modules JavaScript pour extraire les components dans différents fichiers.

App.js

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, ScrollView, Switch, Button } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import Title from './Title.js';
import Input from './Input.js';
import TodoView from './TodoView.js';

export default function App() {
  return (
    <>
        <StatusBar style="auto" />

        <SafeAreaProvider>
            <SafeAreaView style={styles.container}>

            <Title text="Todoer" />

              <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>

                <Input 
                  placeholder='New task'
                  style={{ height: 48, flexGrow: 1 }} 
                /> 

                <Switch />

              </View>

              <Input 
                placeholder='Optional description'
                style={{ width: '100%', height: 96, verticalAlign: 'top' }} 
                multiline={ true } 
              />

              <View style={{ flexDirection: 'row', justifyContent: 'center' }}>
              {/* Envelopper dans une View pour eviter une largeur de 100% */}

                  <Button 
                    title='Add' 
                    color='green' 
                  />

              </View>

              {/* Pas la facon ideale pour afficher une liste, simplification pour la demonstration */}
              <ScrollView style={{ flexGrow: 1 }}>
              {
                [...Array(10).keys()].map((item, index, array) => {
                    return <TodoView todo={item} isLast={index + 1 == array.length} /> 
                })
              }
              </ScrollView>

            </SafeAreaView>
        </SafeAreaProvider>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    gap: 16,
    justifyContent: 'center',
    padding: 16,
  },
});

Title.js

import { Text } from 'react-native';

export default function Title({ text }) {
  return <Text style={{ fontSize: 28, fontWeight: 'bold', textAlign: 'center' }}>{ text }</Text>
}

Input.js

import { StyleSheet, TextInput } from 'react-native';

export default function Input(props) {
    const {style, ...otherProps} = props;

    return <TextInput {...otherProps} style={[styles.input, style]} />
}

const styles = StyleSheet.create({
  input: {
    borderWidth: 1,
    borderColor: 'lightgray',
    padding: 8,
  },
});

TodoView.js

import { StyleSheet, View, Text, TouchableHighlight } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

export default function TodoView({ todo, isLast }) {
  const number = Number(todo);

  return (
    <TouchableHighlight key={todo} onPress={ () => {} }>
        <View 
          style={{ 
            flexDirection: 'row',
            paddingVertical: 16, 
            paddingHorizontal: 8,
            backgroundColor: 'white', /* requis pour highlight */
            borderBottomWidth: isLast ? 0 : 1,
            borderBottomColor: 'lightgray'
          }}
        >
            <Ionicons name="checkmark-circle" size={22} color={ number % 5 == 0 ? 'green' : 'lightgray' } />

            <View style={{ flex: 1, flexDirection: 'column' }}>
                <Text style={[styles.text, { fontWeight: 'bold' }]}>TODO { todo }</Text>
                { 
                  number > 0 &&
                  <Text style={ styles.text }>{ 'Description '.repeat(number) }{ todo }</Text>
                }
            </View>
        </View>
    </TouchableHighlight>
  )
}

const styles = StyleSheet.create({
  text: {
    fontSize: 18,
  },
});

Interactions

Installer Root Toast

npx expo install react-native-root-toast

App.js

...

import { RootSiblingParent } from 'react-native-root-siblings';
import Toast from 'react-native-root-toast';

...

export default function App() {

  function add() {
    Toast.show('Provide a Todo name', {
      duration: Toast.durations.SHORT,
      backgroundColor: 'red',
      textColor: 'white',
    });
  }

  return (
    <RootSiblingParent>
        <StatusBar style="auto" />

        <SafeAreaProvider>
            <SafeAreaView style={styles.container}>

              ...

              <View style={{ flexDirection: 'row', justifyContent: 'center' }}>
              {/* Envelopper dans une View pour eviter une largeur de 100% */}

                  <Button 
                    title='Add' 
                    color='green' 
                    onPress={ add }
                  />

              </View>

              ...

            </SafeAreaView>
        </SafeAreaProvider>
    </RootSiblingParent>
  );
}

TodoView.js

import { StyleSheet, View, Text, TouchableHighlight, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

export default function TodoView({ todo, isLast }) {
  const number = Number(todo);

  function toggleCompleted() {
    Alert.alert(null, `TODO ${todo}`, [
      {
          text: 'Cancel',
          onPress: () => console.log('Cancel Pressed'),
          style: 'cancel',
      },
      {
          text: 'Completed/Incomplete', 
          onPress: () => console.log('Completed/Incomplete Pressed')
      },
    ]);
  }

  return (
    <TouchableHighlight key={todo} onPress={ toggleCompleted }>
        ...
    </TouchableHighlight>
  )
}

State

React exprime pleinement sa puissance lorsqu'on utilise son mécanisme réactif qui permets aux components de se mettre à jour si les données changent.

On parle d'interface déclarative où on décrit le résultat final attendu, en opposition à une approche impérative dans laquelle on énonce les changements à effectuer. Le framework/librairie s'occupe de générer les étapes à notre place. Ex: Commander un plat VS Énumérer les étapes de la préparation.

  • Attention la réactivité est déclenchée lorsqu'une référence change, donc il faut créer des nouveaux objets en mémoire pour la provoquer. On appelle cette mécanique immutabilité.

App.js

import { useState } from 'react';

...

export default function App() {

  const EMPTY_TODO = () => { return {
    name: null,
    done: false
  }}

  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState(EMPTY_TODO());

  function add() {
    if ((newTodo.name?.trim() ?? '') == '') {
        Toast.show('Provide a Todo name', {
          duration: Toast.durations.SHORT,
          backgroundColor: 'red',
          textColor: 'white',
        });
    } else {
        newTodo.id = Math.random().toString(16).substring(2);
        setTodos([newTodo, ...todos]);
        setNewTodo(EMPTY_TODO());
    }
  }

  function toggle(id) {
    setTodos(
        todos.map( t => {
            if (t.id == id) {
                return {
                    ...t, 
                    done : !t.done 
                }
            }

            return t;
        })
    )
  }

  function list() {
    if (todos.length == 0) {
        return (
            <View style={{ flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
                <Text style={{ fontSize: 48, color: 'gray' }}>No todos...</Text>
            </View>
        )
    } else {
      /* Pas la facon ideale pour afficher une liste, simplification pour la demonstration */
      return (
          <ScrollView style={{ flexGrow: 1 }}>
          {
            todos.map((item, index, array) => {
                return <TodoView key={ item.id } todo={item} isLast={index + 1 == array.length} toggle={ toggle } /> 
            })
          }
          </ScrollView>
      )
    }
  }

  return (
    <RootSiblingParent>
        <StatusBar style="auto" />

        <SafeAreaProvider>
            <SafeAreaView style={styles.container}>

            ...

                <Input 
                  placeholder='New task'
                  style={{ height: 48, flexGrow: 1 }} 
                  value={ newTodo.name }
                  onChangeText={ (text) => setNewTodo({...newTodo, name: text}) }
                /> 

                <Switch 
                  value={ newTodo.done }
                  onValueChange={ checked => setNewTodo({...newTodo, done: checked}) } />

              </View>

              ...

              { list() }

            </SafeAreaView>
        </SafeAreaProvider>
    </RootSiblingParent>
  );
}

TodoView.js

...

export default function TodoView({ todo, isLast, toggle }) {

  function toggleCompleted() {
    Alert.alert(null, `${todo.name}`, [
      {
          text: 'Cancel',
          onPress: () => { },
          style: 'cancel',
      },
      {
          text: todo.done ? 'Incomplete' : 'Completed', 
          onPress: () => toggle(todo.id)
      },
    ]);
  }

  return (
    <TouchableHighlight key={todo} onPress={ toggleCompleted }>
        <View>
            <Ionicons name="checkmark-circle" size={22} color={  todo.done ? 'green' : 'lightgray' } />

            <View style={{ flex: 1, flexDirection: 'column' }}>
                <Text style={[styles.text, { fontWeight: 'bold' }]}>{ todo.name }</Text>

                ...

            </View>
        </View>
    </TouchableHighlight>
  )
}

...

Debugger/Dev Tools

  • console.log
  • Chrome Dev Tools
    • Debugger
    • REPL
    • Network
  • VS Code? SDK 49+

📚 Expo Debugging Tools

  • Assurez-vous d'ouvrir l'app dans Expo Go avant de démarrer Chrome Dev Tools (en local uniquement, pas tunnel)

[DÉFI] 🚀

  • Gérer le champ description des todos.
  • Implémenter la suppression d'un todo.

Ressources