npx create-expo-app todo
cd todo
npx expo start
Lancer l'application dans Expo Go.
├── 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',
},
});
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
É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.
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,
},
});
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>
)
}
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.
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>
)
}
...