Développement d'applications mobiles

Développement d'applications mobiles


1 - Environnement de développement

2 - Introduction à React Native

3 - Interface Utilisateur

Ressources

ReactJS

ReactNative

Initialisation du projet

bash
copier
npx create-expo-app demo --template blank

cd demo

# Version Web
npx expo install react-dom react-native-web

# Utilitaire d'affichage appareils mobiles
npx expo install react-native-safe-area-context

Structure du projet

bash
├── App.js # Composant racine
├── index.js # Point d'entrée du code, package.json > main: ...
├── 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

JSX

Le langage de templating de React Native

Code
jsx
copier
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, TextInput, Switch, Button, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';

export default function App() {
return (
<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>
</SafeAreaView>
</SafeAreaProvider>
);
}

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

State

Mécanisme de réactivité de React, suivi des changements et mise à jour/re-render des components

jsx
copier
import { useState } from 'react';

const [name, setName] = useState('');
const [completed, setCompleted] = useState(false);

// ...

<TextInput
placeholder='New task'
style={[styles.input, { height: 48, flexGrow: 1 }]}
value={ name }
onChangeText={ (text) => setName(text) }
/>

<Switch
value={ completed }
onValueChange={ (value) => setCompleted(value) }
/>

// ...

<Text>{ JSON.stringify({ name, completed }) }</Text>
jsx
copier
import { useState } from 'react';

const [todos, setTodos] = useState([])

function handleAdd() {
setTodos([
...todos,
{
name,
completed,
}
])

setName('')
setCompleted(false)
}

// ...

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

onPress={ handleAdd }
/>

// ...

<Text>{ JSON.stringify(todos) }</Text>
Code

On favorise le regroupe de states reliés dans un objet!

  • newTodo

jsx
copier
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, TextInput, Switch, Button, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { useState } from 'react';

export default function App() {
const [newTodo, setNewTodo] = useState({ name: '', completed: false })
const [todos, setTodos] = useState([])

function handleAdd() {
setTodos([
...todos,
newTodo
])

setNewTodo({ name: '', completed: false })
}

return (
<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 }]}
value={ newTodo.name }
onChangeText={ (text) => setNewTodo({ ...newTodo, name: text}) }
/>

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

</View>

<Text>{ JSON.stringify(newTodo) }</Text>

<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'

onPress={ handleAdd }
/>
</View>

<Text>{ JSON.stringify(todos) }</Text>
</SafeAreaView>
</SafeAreaProvider>
);
}

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

Components

Mécanisme d'encapsulation et de réutilisation

jsx
Input.js
copier
import { StyleSheet, TextInput } from 'react-native';

export default function Input({style, ...otherProps}) {
return (
<TextInput
style={[styles.input, style]}
{...otherProps}
/>
)
}

const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: 'lightgray',
padding: 8,
},
});
jsx
App.js
copier
import Input from './Input';

//...
// TextInput -> Input
// Menage des StyleSheet inutiles

Debugging

Les méthodes de trace traditionnelles sont tout à fait pertinentes!

L'utilisation du debugger via l'environnement distant de la machine virtuelle est plutôt ardue, on peut exécuter la version Web et utiliser l'inspecteur du navigateur. L'extension navigateur React Dev Tools peut offrir des informations spécifiques à la hiérarchie de components.

Sinon, en mode local, une version dédiée des outils développeurs est accessible en appuyant sur j dans le terminal du serveur de développement.

Todoer

bash
copier
# Ajouter quelques utilitaires multi-plateformes
npx expo install toastify-react-native @expo/vector-icons react-native-simple-dialogs

Départ

jsx
App.js
copier
import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, TouchableHighlight, ScrollView, Switch, Button, Text, TextInput } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import ToastManager, { Toast } from 'toastify-react-native'
import { Dialog } from 'react-native-simple-dialogs';
import { Ionicons } from '@expo/vector-icons';


function TodoItem({ todo, isLast, setToggling }) {
return (
<TouchableHighlight onPress={ () => setToggling(todo) }>
<View
style={{
flexDirection: 'row',
gap: 8,
paddingVertical: 16,
paddingHorizontal: 8,
backgroundColor: 'white', /* requis pour highlight */
borderBottomWidth: isLast ? 0 : 1,
borderBottomColor: 'lightgray'
}}
>
<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>
{
!!todo.description &&
<Text style={ styles.text }>{ todo.description }</Text>
}
</View>
</View>
</TouchableHighlight>
)
}

export default function App() {

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

const SEED_COUNT = 4;
const SEED = [...Array(SEED_COUNT).keys()].map((item, index, array) => {
const name = `Todo ${item}`

return {
id: Math.random().toString(16).substring(2),
name: name,
description: `Description ${name} `.repeat(index),
done: (index % 3)
}
})

const [todos, setTodos] = useState(SEED);
const [newTodo, setNewTodo] = useState(EMPTY_TODO());
const [toggling, setToggling] = useState(null)

function handleAdd() {
if ((newTodo.name?.trim() ?? '') == '') {
Toast.error('Provide a Todo name', 'bottom')
} else {
newTodo.id = Math.random().toString(16).substring(2);
newTodo.name = newTodo.name.trim()

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((todo, index, array) => {
return (
<TodoItem
key={todo.id}
todo={ todo }
isLast={ index + 1 == array.length }
setToggling={ setToggling }
/>
)
})
}
</ScrollView>
)
}
}

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

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

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

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

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

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

</View>

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

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

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

</View>

{ list() }

<Dialog
title={ toggling?.name }
visible={ !!toggling }
onTouchOutside={ () => setToggling(null) }
animationType="fade"
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 32 }}>
<Button title="Cancel" color="gray" onPress={ () => setToggling(null) } />
<Button title={ toggling?.done ? 'Incomplete' : 'Completed' } onPress={ () => { setToggling(false); toggle(toggling?.id); } } />
</View>
</Dialog>

</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,
},
});

Go