RN Lezione 14: Progetto — Sviluppo CRUD
Obiettivi
Section titled “Obiettivi”- Implementare CRUD completo (Create, Read, Update, Delete)
- Collegare form → salvataggio → visualizzazione
- Usare lo stato globale tra schermate
1. Pattern CRUD in React Native
Section titled “1. Pattern CRUD in React Native”Ogni app ha bisogno di queste operazioni:
| Operazione | Azione | Schermata |
|---|---|---|
| Create | Aggiungere elemento | Form / Aggiungi |
| Read | Visualizzare elementi | Home / Lista |
| Update | Modificare elemento | Modifica / Dettaglio |
| Delete | Eliminare elemento | Con Alert |
Schema flusso dati
Section titled “Schema flusso dati”[Form Aggiungi] → salvaDati(nuovo) → AsyncStorage ↓[Home Lista] ← caricaDati() ← AsyncStorage │ ├→ clicca → [Dettaglio] (read-only) │ └→ long-press → Alert → eliminaDati(id)2. Pattern: passare dati tra schermate
Section titled “2. Pattern: passare dati tra schermate”Opzione 1: Stato nell’App e props
Section titled “Opzione 1: Stato nell’App e props”// app/_layout.js — stato centraleimport { useState, useEffect } from 'react';import { Stack } from 'expo-router';import { caricaDati, salvaDati } from '../utils/storage';
export default function Layout() { const [dati, setDati] = useState([]);
useEffect(() => { caricaDati().then(setDati); }, []);
useEffect(() => { if (dati.length) salvaDati(dati); }, [dati]);
// Passa dati alle schermate via context...}Opzione 2: Parametri URL (semplice)
Section titled “Opzione 2: Parametri URL (semplice)”// Da Home → Dettaglio (passa ID)router.push({ pathname: '/dettaglio', params: { id: item.id }});
// In Dettaglio — carica dati da AsyncStorageconst { id } = useLocalSearchParams();const [item, setItem] = useState(null);
useEffect(() => { const carica = async () => { const dati = await caricaDati(); const trovato = dati.find(d => d.id === id); setItem(trovato); }; carica();}, [id]);3. Pattern: form di aggiunta
Section titled “3. Pattern: form di aggiunta”import { useState } from 'react';import { View, Text, TextInput, TouchableOpacity, Alert, StyleSheet } from 'react-native';import { router } from 'expo-router';import AsyncStorage from '@react-native-async-storage/async-storage';
const CHIAVE = '@app_dati';
export default function Aggiungi() { const [titolo, setTitolo] = useState(''); const [testo, setTesto] = useState(''); const [salvataggio, setSalvataggio] = useState(false);
const salva = async () => { if (!titolo.trim()) { Alert.alert('Errore', 'Il titolo è obbligatorio'); return; }
setSalvataggio(true); try { // 1. Carica dati esistenti const json = await AsyncStorage.getItem(CHIAVE); const dati = json ? JSON.parse(json) : [];
// 2. Aggiungi nuovo elemento const nuovo = { id: Date.now().toString(), titolo: titolo.trim(), testo: testo.trim(), data: new Date().toLocaleDateString('it-IT'), creatoIl: new Date().toISOString(), };
dati.push(nuovo);
// 3. Salva await AsyncStorage.setItem(CHIAVE, JSON.stringify(dati));
Alert.alert('✅ Salvato', 'Elemento aggiunto con successo!'); router.back(); // ← torna alla lista } catch (err) { Alert.alert('❌ Errore', 'Impossibile salvare i dati'); } finally { setSalvataggio(false); } };
return ( <View style={styles.container}> <Text style={styles.label}>Titolo</Text> <TextInput style={styles.input} value={titolo} onChangeText={setTitolo} placeholder="Inserisci titolo..." />
<Text style={styles.label}>Testo</Text> <TextInput style={[styles.input, styles.textarea]} value={testo} onChangeText={setTesto} placeholder="Inserisci testo..." multiline numberOfLines={5} />
<TouchableOpacity style={[styles.bottone, salvataggio && styles.bottoneDisabile]} onPress={salva} disabled={salvataggio} > <Text style={styles.bottoneTesto}> {salvataggio ? 'Salvataggio...' : '💾 Salva'} </Text> </TouchableOpacity> </View> );}
const styles = StyleSheet.create({ container: { flex: 1, padding: 20, backgroundColor: '#f5f5f5' }, label: { fontSize: 16, fontWeight: 'bold', marginBottom: 5, marginTop: 15 }, input: { backgroundColor: 'white', padding: 12, borderRadius: 8, borderWidth: 1, borderColor: '#ddd', fontSize: 16, }, textarea: { height: 120, textAlignVertical: 'top' }, bottone: { backgroundColor: '#3498db', padding: 15, borderRadius: 8, alignItems: 'center', marginTop: 20, }, bottoneDisabile: { opacity: 0.6 }, bottoneTesto: { color: 'white', fontWeight: 'bold', fontSize: 16 },});4. Pattern: schermata dettaglio
Section titled “4. Pattern: schermata dettaglio”import { View, Text, TouchableOpacity, Alert, StyleSheet } from 'react-native';import { useLocalSearchParams, router } from 'expo-router';import { useState, useEffect } from 'react';import AsyncStorage from '@react-native-async-storage/async-storage';
const CHIAVE = '@app_dati';
export default function Dettaglio() { const { id } = useLocalSearchParams(); const [item, setItem] = useState(null);
useEffect(() => { const carica = async () => { const json = await AsyncStorage.getItem(CHIAVE); const dati = json ? JSON.parse(json) : []; const trovato = dati.find(d => d.id === id); setItem(trovato); }; carica(); }, [id]);
const elimina = () => { Alert.alert( 'Elimina', 'Sei sicuro?', [ { text: 'Annulla', style: 'cancel' }, { text: 'Elimina', style: 'destructive', onPress: async () => { const json = await AsyncStorage.getItem(CHIAVE); const dati = json ? JSON.parse(json) : []; const filtrati = dati.filter(d => d.id !== id); await AsyncStorage.setItem(CHIAVE, JSON.stringify(filtrati)); router.back(); } }, ] ); };
if (!item) { return ( <View style={styles.center}> <Text>Caricamento...</Text> </View> ); }
return ( <View style={styles.container}> <Text style={styles.titolo}>{item.titolo}</Text> <Text style={styles.data}>{item.data}</Text> <Text style={styles.testo}>{item.testo}</Text> <TouchableOpacity style={styles.btnElimina} onPress={elimina}> <Text style={styles.btnEliminaTesto}>🗑️ Elimina</Text> </TouchableOpacity> </View> );}5. Pattern: modifica elemento
Section titled “5. Pattern: modifica elemento”// Aggiungi con logica di modifica (se params.id esiste)const { id } = useLocalSearchParams();const [titolo, setTitolo] = useState('');const isModifica = Boolean(id);
useEffect(() => { if (id) { // Carica dati esistenti per modificarli const carica = async () => { const json = await AsyncStorage.getItem(CHIAVE); const dati = json ? JSON.parse(json) : []; const trovato = dati.find(d => d.id === id); if (trovato) { setTitolo(trovato.titolo); setTesto(trovato.testo); } }; carica(); }}, [id]);
const salva = async () => { // ... stessa logica, ma se isModifica → aggiorna invece di creare if (isModifica) { dati = dati.map(d => d.id === id ? { ...d, titolo, testo } : d); } else { dati.push({ id: Date.now().toString(), titolo, testo, data: ... }); }};6. Pattern: stato di caricamento globale
Section titled “6. Pattern: stato di caricamento globale”import { View, ActivityIndicator, Text } from 'react-native';
export default function Loading({ messaggio = 'Caricamento...' }) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5', }}> <ActivityIndicator size="large" color="#3498db" /> <Text style={{ marginTop: 10, color: 'grey' }}>{messaggio}</Text> </View> );}7. Checklist sviluppo
Section titled “7. Checklist sviluppo”- Create: form aggiunge elemento → AsyncStorage
- Read: FlatList mostra elementi da AsyncStorage
- Update: form di modifica aggiorna elemento
- Delete: long-press o bottone elimina con conferma
- Validazione campi form
- Gestione lista vuota
- Navigazione Home ↔ Aggiungi ↔ Dettaglio
- Test su Expo Go: CRUD funziona su telefono