Développement mobile : Native vs Hybride vs PWA – Guide complet 2025
Le choix de la technologie pour développer une application mobile est crucial. Cette décision impacte…
React Native permet de créer des applications mobiles natives pour iOS et Android en utilisant JavaScript et React. Dans ce guide complet, nous allons créer ensemble une application de gestion de tâches professionnelle, de l’installation à la publication.
Pour développer sur iOS et Android :
Outils de développement :
# 1. Vérifier Node.js (version 18+)
node --version
npm --version
# 2. Installer React Native CLI
npm install -g react-native-cli
# 3. Installer Expo CLI (optionnel, recommandé pour débuter)
npm install -g expo-cli
# 4. Vérifier l'installation
npx react-native --version
# 5. Installer Watchman (macOS, pour le hot reload)
brew install watchman
# 6. Installer CocoaPods (macOS, pour iOS)
sudo gem install cocoapods
# Télécharger Android Studio depuis developer.android.com
# Configurer les variables d'environnement (Linux/macOS)
# Ajouter à ~/.bashrc ou ~/.zshrc
export ANDROIDHOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROIDHOME/emulator
export PATH=$PATH:$ANDROIDHOME/platform-tools
export PATH=$PATH:$ANDROIDHOME/tools
export PATH=$PATH:$ANDROIDHOME/tools/bin
# Recharger la configuration
source ~/.zshrc
# Vérifier la configuration
echo $ANDROIDHOME
# Accepter les licences
yes | sdkmanager --licenses
# Installer Xcode depuis l'App Store
# Installer les Command Line Tools
xcode-select --install
# Accepter la licence
sudo xcodebuild -license accept
# Vérifier l'installation
xcodebuild -version
# Créer le projet avec template TypeScript
npx react-native init TodoApp --template react-native-template-typescript
# Naviguer dans le projet
cd TodoApp
# Structure du projet
# TodoApp/
# ├── android/ # Projet Android natif
# ├── ios/ # Projet iOS natif
# ├── src/ # Code source (à créer)
# ├── App.tsx # Composant racine
# ├── package.json # Dépendances
# ├── tsconfig.json # Configuration TypeScript
# └── metro.config.js # Bundler Metro
# Créer la structure src/
mkdir -p src/{components,screens,navigation,utils,types,hooks,services}
# Navigation
npm install @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs
# Dépendances navigation
npm install react-native-screens react-native-safe-area-context react-native-gesture-handler
# Stockage local
npm install @react-native-async-storage/async-storage
# Gestion d'état
npm install zustand
# Utilitaires
npm install date-fns react-native-uuid
# Types TypeScript
npm install -D @types/react @types/react-native
# iOS : Installation des pods
cd ios && pod install && cd ..
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["es2021"],
"jsx": "react-native",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/": ["src/"],
"@components/": ["src/components/"],
"@screens/": ["src/screens/"],
"@navigation/": ["src/navigation/"],
"@utils/": ["src/utils/"],
"@types/": ["src/types/"],
"@hooks/": ["src/hooks/"],
"@services/": ["src/services/"]
}
},
"include": ["src//", "App.tsx"],
"exclude": ["nodemodules", "android", "ios"]
}
// src/types/todo.ts
export interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate: string | null;
createdAt: string;
updatedAt: string;
category: string;
}
export interface TodoCategory {
id: string;
name: string;
color: string;
icon: string;
}
export type TodoFilter = 'all' | 'active' | 'completed';
export type TodoSort = 'date' | 'priority' | 'alphabetical';
// src/types/navigation.ts
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { Todo } from './todo';
export type RootStackParamList = {
TodoList: undefined;
TodoDetail: { todoId: string };
AddTodo: { category?: string };
EditTodo: { todo: Todo };
Settings: undefined;
};
export type TodoListNavigationProp = StackNavigationProp<
RootStackParamList,
'TodoList'
>;
export type TodoDetailNavigationProp = StackNavigationProp<
RootStackParamList,
'TodoDetail'
>;
export type TodoDetailRouteProp = RouteProp;
export type AddTodoNavigationProp = StackNavigationProp<
RootStackParamList,
'AddTodo'
>;
export type EditTodoNavigationProp = StackNavigationProp<
RootStackParamList,
'EditTodo'
>;
export type EditTodoRouteProp = RouteProp;
// src/services/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Todo, TodoCategory } from '@/types/todo';
const TODOSKEY = '@todos';
const CATEGORIESKEY = '@categories';
class StorageService {
// Gestion des todos
async getTodos(): Promise {
try {
const todosJson = await AsyncStorage.getItem(TODOS KEY);
return todosJson ? JSON.parse(todosJson) : [];
} catch (error) {
console.error('Erreur lecture todos:', error);
return [];
}
}
async saveTodos(todos: Todo[]): Promise {
try {
await AsyncStorage.setItem(TODOSKEY, JSON.stringify(todos));
} catch (error) {
console.error('Erreur sauvegarde todos:', error);
throw new Error('Impossible de sauvegarder les tâches');
}
}
async getTodoById(id: string): Promise {
try {
const todos = await this.getTodos();
return todos.find(todo => todo.id === id) || null;
} catch (error) {
console.error('Erreur récupération todo:', error);
return null;
}
}
async addTodo(todo: Todo): Promise {
try {
const todos = await this.getTodos();
const newTodos = [...todos, todo];
await this.saveTodos(newTodos);
return todo;
} catch (error) {
console.error('Erreur ajout todo:', error);
throw new Error('Impossible d'ajouter la tâche');
}
}
async updateTodo(id: string, updates: Partial): Promise {
try {
const todos = await this.getTodos();
const index = todos.findIndex(todo => todo.id === id);
if (index === -1) {
throw new Error('Tâche introuvable');
}
const updatedTodo = {
...todos[index],
...updates,
updatedAt: new Date().toISOString(),
};
todos[index] = updatedTodo;
await this.saveTodos(todos);
return updatedTodo;
} catch (error) {
console.error('Erreur mise à jour todo:', error);
throw new Error('Impossible de mettre à jour la tâche');
}
}
async deleteTodo(id: string): Promise {
try {
const todos = await this.getTodos();
const filteredTodos = todos.filter(todo => todo.id !== id);
await this.saveTodos(filteredTodos);
} catch (error) {
console.error('Erreur suppression todo:', error);
throw new Error('Impossible de supprimer la tâche');
}
}
async toggleTodoComplete(id: string): Promise {
try {
const todos = await this.getTodos();
const todo = todos.find(t => t.id === id);
if (!todo) {
throw new Error('Tâche introuvable');
}
return await this.updateTodo(id, {
completed: !todo.completed,
});
} catch (error) {
console.error('Erreur toggle todo:', error);
throw new Error('Impossible de modifier le statut');
}
}
// Gestion des catégories
async getCategories(): Promise {
try {
const categoriesJson = await AsyncStorage.getItem(CATEGORIES KEY);
if (!categoriesJson) {
const defaultCategories = this.getDefaultCategories();
await this.saveCategories(defaultCategories);
return defaultCategories;
}
return JSON.parse(categoriesJson);
} catch (error) {
console.error('Erreur lecture catégories:', error);
return this.getDefaultCategories();
}
}
async saveCategories(categories: TodoCategory[]): Promise {
try {
await AsyncStorage.setItem(CATEGORIESKEY, JSON.stringify(categories));
} catch (error) {
console.error('Erreur sauvegarde catégories:', error);
throw new Error('Impossible de sauvegarder les catégories');
}
}
private getDefaultCategories(): TodoCategory[] {
return [
{ id: '1', name: 'Personnel', color: '#3B82F6', icon: 'person' },
{ id: '2', name: 'Travail', color: '#10B981', icon: 'briefcase' },
{ id: '3', name: 'Shopping', color: '#F59E0B', icon: 'cart' },
{ id: '4', name: 'Santé', color: '#EF4444', icon: 'heart' },
];
}
// Utilitaires
async clearAllData(): Promise {
try {
await AsyncStorage.multiRemove([TODOS KEY, CATEGORIESKEY]);
} catch (error) {
console.error('Erreur suppression données:', error);
throw new Error('Impossible de supprimer les données');
}
}
}
export default new StorageService();
// src/store/todoStore.ts
import { create } from 'zustand';
import { Todo, TodoFilter, TodoSort } from '@/types/todo';
import storageService from '@/services/storage';
import uuid from 'react-native-uuid';
interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
filter: TodoFilter;
sortBy: TodoSort;
// Actions
loadTodos: () => Promise;
addTodo: (
title: string,
description: string,
priority: Todo['priority'],
dueDate: string | null,
category: string
) => Promise;
updateTodo: (id: string, updates: Partial) => Promise;
deleteTodo: (id: string) => Promise;
toggleComplete: (id: string) => Promise;
setFilter: (filter: TodoFilter) => void;
setSortBy: (sort: TodoSort) => void;
clearError: () => void;
// Getters
getFilteredTodos: () => Todo[];
getTodosByCategory: (category: string) => Todo[];
getStats: () => {
total: number;
completed: number;
active: number;
overdue: number;
};
}
export const useTodoStore = create((set, get) => ({
todos: [],
loading: false,
error: null,
filter: 'all',
sortBy: 'date',
loadTodos: async () => {
set({ loading: true, error: null });
try {
const todos = await storageService.getTodos();
set({ todos, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Erreur de chargement',
loading: false,
});
}
},
addTodo: async (title, description, priority, dueDate, category) => {
set({ loading: true, error: null });
try {
const newTodo: Todo = {
id: uuid.v4() as string,
title,
description,
completed: false,
priority,
dueDate,
category,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await storageService.addTodo(newTodo);
const todos = await storageService.getTodos();
set({ todos, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Erreur d'ajout',
loading: false,
});
}
},
updateTodo: async (id, updates) => {
set({ loading: true, error: null });
try {
await storageService.updateTodo(id, updates);
const todos = await storageService.getTodos();
set({ todos, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Erreur de mise à jour',
loading: false,
});
}
},
deleteTodo: async (id) => {
set({ loading: true, error: null });
try {
await storageService.deleteTodo(id);
const todos = await storageService.getTodos();
set({ todos, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Erreur de suppression',
loading: false,
});
}
},
toggleComplete: async (id) => {
try {
await storageService.toggleTodoComplete(id);
const todos = await storageService.getTodos();
set({ todos });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Erreur de modification',
});
}
},
setFilter: (filter) => set({ filter }),
setSortBy: (sortBy) => set({ sortBy }),
clearError: () => set({ error: null }),
getFilteredTodos: () => {
const { todos, filter, sortBy } = get();
let filtered = todos;
// Filtrage
if (filter === 'active') {
filtered = todos.filter(todo => !todo.completed);
} else if (filter === 'completed') {
filtered = todos.filter(todo => todo.completed);
}
// Tri
const sorted = [...filtered].sort((a, b) => {
switch (sortBy) {
case 'date':
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
case 'priority':
const priorityOrder = { high: 0, medium: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
case 'alphabetical':
return a.title.localeCompare(b.title);
default:
return 0;
}
});
return sorted;
},
getTodosByCategory: (category) => {
const { todos } = get();
return todos.filter(todo => todo.category === category);
},
getStats: () => {
const { todos } = get();
const now = new Date();
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
overdue: todos.filter(t => {
if (!t.dueDate || t.completed) return false;
return new Date(t.dueDate) < now;
}).length,
};
},
}));
// src/components/TodoCard.tsx
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Animated,
} from 'react-native';
import { Todo } from '@/types/todo';
import { format, isPast } from 'date-fns';
import { fr } from 'date-fns/locale';
interface TodoCardProps {
todo: Todo;
onPress: () => void;
onToggleComplete: () => void;
onDelete: () => void;
}
export const TodoCard: React.FC = ({
todo,
onPress,
onToggleComplete,
onDelete,
}) => {
const scaleAnim = React.useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.97,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
useNativeDriver: true,
}).start();
};
const getPriorityColor = () => {
switch (todo.priority) {
case 'high':
return '#EF4444';
case 'medium':
return '#F59E0B';
case 'low':
return '#10B981';
default:
return '#6B7280';
}
};
const isOverdue = todo.dueDate && isPast(new Date(todo.dueDate)) && !todo.completed;
return (
{todo.completed && ✓ }
{todo.title}
{todo.description ? (
{todo.description}
) : null}
{todo.priority === 'high' && 'Haute'}
{todo.priority === 'medium' && 'Moyenne'}
{todo.priority === 'low' && 'Basse'}
{todo.dueDate && (
{format(new Date(todo.dueDate), 'dd MMM yyyy', { locale: fr })}
)}
×
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 12,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
completedCard: {
opacity: 0.7,
},
header: {
flexDirection: 'row',
alignItems: 'flex-start',
},
checkboxContainer: {
marginRight: 12,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: '#D1D5DB',
alignItems: 'center',
justifyContent: 'center',
},
checkboxChecked: {
backgroundColor: '#3B82F6',
borderColor: '#3B82F6',
},
checkmark: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
},
content: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#1F2937',
marginBottom: 4,
},
completedTitle: {
textDecorationLine: 'line-through',
color: '#9CA3AF',
},
description: {
fontSize: 14,
color: '#6B7280',
marginBottom: 8,
},
metadata: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
priorityBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
priorityText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
},
dueDate: {
fontSize: 12,
color: '#6B7280',
},
overdue: {
color: '#EF4444',
fontWeight: '600',
},
deleteButton: {
marginLeft: 8,
},
deleteIcon: {
fontSize: 28,
color: '#EF4444',
fontWeight: '300',
},
});
// src/components/Button.tsx
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'danger';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
fullWidth?: boolean;
}
export const Button: React.FC = ({
title,
onPress,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
style,
textStyle,
fullWidth = false,
}) => {
const buttonStyles = [
styles.button,
styles[${variant}Button],
styles[${size}Button],
fullWidth && styles.fullWidth,
(disabled || loading) && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[${variant}Text],
styles[${size}Text],
textStyle,
];
return (
{loading ? (
) : (
{title}
)}
);
};
const styles = StyleSheet.create({
button: {
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
fullWidth: {
width: '100%',
},
disabled: {
opacity: 0.5,
},
// Variants
primaryButton: {
backgroundColor: '#3B82F6',
},
secondaryButton: {
backgroundColor: '#6B7280',
},
outlineButton: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: '#3B82F6',
},
dangerButton: {
backgroundColor: '#EF4444',
},
// Sizes
smallButton: {
paddingHorizontal: 12,
paddingVertical: 6,
},
mediumButton: {
paddingHorizontal: 16,
paddingVertical: 12,
},
largeButton: {
paddingHorizontal: 24,
paddingVertical: 16,
},
// Text styles
text: {
fontWeight: '600',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#FFFFFF',
},
outlineText: {
color: '#3B82F6',
},
dangerText: {
color: '#FFFFFF',
},
smallText: {
fontSize: 14,
},
mediumText: {
fontSize: 16,
},
largeText: {
fontSize: 18,
},
});
// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { RootStackParamList } from '@/types/navigation';
// Screens
import TodoListScreen from '@/screens/TodoListScreen';
import TodoDetailScreen from '@/screens/TodoDetailScreen';
import AddTodoScreen from '@/screens/AddTodoScreen';
import EditTodoScreen from '@/screens/EditTodoScreen';
import SettingsScreen from '@/screens/SettingsScreen';
const Stack = createStackNavigator();
export const AppNavigator: React.FC = () => {
return (
{
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
],
},
};
},
}}
>
);
};
// src/screens/TodoListScreen.tsx
import React, { useEffect, useState } from 'react';
import {
View,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
RefreshControl,
Alert,
SafeAreaView,
} from 'react-native';
import { TodoListNavigationProp } from '@/types/navigation';
import { useTodoStore } from '@/store/todoStore';
import { TodoCard } from '@/components/TodoCard';
import { Button } from '@/components/Button';
import { Todo, TodoFilter, TodoSort } from '@/types/todo';
interface Props {
navigation: TodoListNavigationProp;
}
const TodoListScreen: React.FC = ({ navigation }) => {
const {
loadTodos,
deleteTodo,
toggleComplete,
getFilteredTodos,
getStats,
setFilter,
setSortBy,
filter,
sortBy,
loading,
error,
clearError,
} = useTodoStore();
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadTodos();
}, []);
useEffect(() => {
if (error) {
Alert.alert('Erreur', error, [
{ text: 'OK', onPress: clearError },
]);
}
}, [error]);
const handleRefresh = async () => {
setRefreshing(true);
await loadTodos();
setRefreshing(false);
};
const handleDeleteTodo = (id: string) => {
Alert.alert(
'Confirmation',
'Voulez-vous vraiment supprimer cette tâche ?',
[
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: () => deleteTodo(id),
},
]
);
};
const todos = getFilteredTodos();
const stats = getStats();
const renderTodo = ({ item }: { item: Todo }) => (
navigation.navigate('TodoDetail', { todoId: item.id })}
onToggleComplete={() => toggleComplete(item.id)}
onDelete={() => handleDeleteTodo(item.id)}
/>
);
const renderHeader = () => (
{stats.total}
Total
{stats.completed}
Terminées
{stats.active}
Actives
{stats.overdue > 0 && (
{stats.overdue}
En retard
)}
setFilter('all')}
>
Toutes
setFilter('active')}
>
Actives
setFilter('completed')}
>
Terminées
Trier par :
setSortBy('date')}
>
Date
setSortBy('priority')}
>
Priorité
setSortBy('alphabetical')}
>
A-Z
);
const renderEmpty = () => (
📝
Aucune tâche
{filter === 'all'
? 'Ajoutez votre première tâche'
: filter === 'active'
? 'Aucune tâche active'
: 'Aucune tâche terminée'}
);
return (
item.id}
contentContainerStyle={styles.listContent}
ListHeaderComponent={renderHeader}
ListEmptyComponent={renderEmpty}
refreshControl={
}
/>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F3F4F6',
},
listContent: {
padding: 16,
paddingBottom: 80,
},
header: {
marginBottom: 16,
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
statItem: {
alignItems: 'center',
},
statValue: {
fontSize: 24,
fontWeight: 'bold',
color: '#1F2937',
},
statLabel: {
fontSize: 12,
color: '#6B7280',
marginTop: 4,
},
filters: {
flexDirection: 'row',
gap: 8,
marginBottom: 16,
},
filterButton: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
backgroundColor: '#FFFFFF',
borderRadius: 8,
borderWidth: 1,
borderColor: '#E5E7EB',
alignItems: 'center',
},
activeFilter: {
backgroundColor: '#3B82F6',
borderColor: '#3B82F6',
},
filterText: {
fontSize: 14,
color: '#6B7280',
fontWeight: '500',
},
activeFilterText: {
color: '#FFFFFF',
},
sortContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
sortLabel: {
fontSize: 14,
color: '#6B7280',
fontWeight: '500',
},
sortButton: {
paddingVertical: 6,
paddingHorizontal: 12,
backgroundColor: '#E5E7EB',
borderRadius: 6,
},
activeSort: {
backgroundColor: '#3B82F6',
},
sortText: {
fontSize: 12,
color: '#1F2937',
fontWeight: '500',
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyEmoji: {
fontSize: 64,
marginBottom: 16,
},
emptyTitle: {
fontSize: 20,
fontWeight: '600',
color: '#1F2937',
marginBottom: 8,
},
emptyText: {
fontSize: 14,
color: '#6B7280',
},
fab: {
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
},
});
export default TodoListScreen;
// src/screens/AddTodoScreen.tsx
import React, { useState } from 'react';
import {
View,
TextInput,
StyleSheet,
ScrollView,
Text,
TouchableOpacity,
Alert,
Platform,
SafeAreaView,
KeyboardAvoidingView,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
import { AddTodoNavigationProp } from '@/types/navigation';
import { useTodoStore } from '@/store/todoStore';
import { Button } from '@/components/Button';
import { Todo } from '@/types/todo';
interface Props {
navigation: AddTodoNavigationProp;
}
const AddTodoScreen: React.FC = ({ navigation }) => {
const { addTodo, loading } = useTodoStore();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState('medium');
const [dueDate, setDueDate] = useState(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const [category, setCategory] = useState('Personnel');
const categories = ['Personnel', 'Travail', 'Shopping', 'Santé'];
const handleSubmit = async () => {
if (!title.trim()) {
Alert.alert('Erreur', 'Le titre est obligatoire');
return;
}
try {
await addTodo(
title.trim(),
description.trim(),
priority,
dueDate ? dueDate.toISOString() : null,
category
);
Alert.alert('Succès', 'Tâche ajoutée avec succès', [
{ text: 'OK', onPress: () => navigation.goBack() },
]);
} catch (error) {
Alert.alert('Erreur', 'Impossible d'ajouter la tâche');
}
};
const handleDateChange = (event: any, selectedDate?: Date) => {
setShowDatePicker(Platform.OS === 'ios');
if (selectedDate) {
setDueDate(selectedDate);
}
};
return (
Titre
Description
Priorité
setPriority('low')}
>
Basse
setPriority('medium')}
>
Moyenne
setPriority('high')}
>
Haute
Catégorie
{categories.map((cat) => (
setCategory(cat)}
>
{cat}
))}
Date d'échéance
setShowDatePicker(true)}
>
{dueDate
? dueDate.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
: 'Sélectionner une date'}
{dueDate && (
setDueDate(null)}
>
Supprimer la date
)}
{showDatePicker && (
)}
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F3F4F6',
},
flex: {
flex: 1,
},
content: {
padding: 16,
},
section: {
marginBottom: 24,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#1F2937',
marginBottom: 8,
},
input: {
backgroundColor: '#FFFFFF',
borderRadius: 8,
padding: 12,
fontSize: 16,
color: '#1F2937',
borderWidth: 1,
borderColor: '#E5E7EB',
},
textArea: {
minHeight: 100,
paddingTop: 12,
},
priorityContainer: {
flexDirection: 'row',
gap: 12,
},
priorityButton: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#FFFFFF',
borderRadius: 8,
borderWidth: 1,
borderColor: '#E5E7EB',
alignItems: 'center',
},
priorityText: {
fontSize: 14,
fontWeight: '600',
color: '#6B7280',
},
priorityTextActive: {
color: '#FFFFFF',
},
categoryContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
categoryButton: {
paddingVertical: 8,
paddingHorizontal: 16,
backgroundColor: '#FFFFFF',
borderRadius: 8,
borderWidth: 1,
borderColor: '#E5E7EB',
},
categoryButtonActive: {
backgroundColor: '#3B82F6',
borderColor: '#3B82F6',
},
categoryText: {
fontSize: 14,
fontWeight: '500',
color: '#6B7280',
},
categoryTextActive: {
color: '#FFFFFF',
},
dateButton: {
backgroundColor: '#FFFFFF',
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: '#E5E7EB',
},
dateButtonText: {
fontSize: 16,
color: '#1F2937',
},
clearDateButton: {
marginTop: 8,
alignSelf: 'flex-start',
},
clearDateText: {
fontSize: 14,
color: '#EF4444',
},
actions: {
flexDirection: 'row',
gap: 12,
marginTop: 16,
},
actionButton: {
flex: 1,
},
});
export default AddTodoScreen;
# iOS (macOS uniquement)
npm run ios
# ou pour un simulateur spécifique
npx react-native run-ios --simulator="iPhone 15 Pro"
# Android
npm run android
# ou pour un émulateur spécifique
npx react-native run-android --deviceId=emulator-5554
# Démarrer Metro Bundler séparément
npm start
// Configuration Reactotron (optionnel)
// npm install --save-dev reactotron-react-native
// ReactotronConfig.ts
import Reactotron from 'reactotron-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
if (DEV) {
Reactotron
.setAsyncStorageHandler(AsyncStorage)
.configure({
name: 'TodoApp',
})
.useReactNative({
asyncStorage: true,
networking: {
ignoreUrls: /symbolicate/,
},
editor: false,
errors: { veto: (stackFrame) => false },
overlay: false,
})
.connect();
}
export default Reactotron;
// tests/TodoCard.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { TodoCard } from '@/components/TodoCard';
import { Todo } from '@/types/todo';
const mockTodo: Todo = {
id: '1',
title: 'Test Todo',
description: 'Test Description',
completed: false,
priority: 'medium',
dueDate: null,
category: 'Personnel',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
describe('TodoCard', () => {
it('affiche correctement le titre et la description', () => {
const { getByText } = render(
);
expect(getByText('Test Todo')).toBeTruthy();
expect(getByText('Test Description')).toBeTruthy();
});
it('appelle onToggleComplete quand on clique sur la checkbox', () => {
const onToggleComplete = jest.fn();
const { getByTestId } = render(
);
fireEvent.press(getByTestId('checkbox'));
expect(onToggleComplete).toHaveBeenCalledTimes(1);
});
});
# Générer une clé de signature (première fois uniquement)
cd android/app
keytool -genkeypair -v -storetype PKCS12 -keystore todo-app-release.keystore -alias todo-app -keyalg RSA -keysize 2048 -validity 10000
# Configurer gradle.properties
# android/gradle.properties
MYAPPRELEASESTOREFILE=todo-app-release.keystore
MYAPPRELEASEKEYALIAS=todo-app
MYAPPRELEASESTOREPASSWORD=yourpassword
MYAPPRELEASEKEYPASSWORD=your_password
# Build release
cd android
./gradlew assembleRelease
# APK disponible dans :
# android/app/build/outputs/apk/release/app-release.apk
# Ouvrir Xcode
open ios/TodoApp.xcworkspace
# Dans Xcode :
# 1. Sélectionner le schéma "TodoApp" > "Generic iOS Device"
# 2. Product > Archive
# 3. Distribute App > App Store Connect
# 4. Suivre l'assistant de publication
Vous avez maintenant une application React Native complète et fonctionnelle. Ce guide couvre les fondamentaux du développement mobile avec React Native : configuration, navigation, gestion d'état, stockage local et interface utilisateur.
Prochaines étapes :
Ressources utiles :**
Continuez à pratiquer et à explorer l'écosystème React Native pour créer des applications mobiles professionnelles.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.