RN Lezione 8: Form complessi e AsyncStorage
Obiettivi
Section titled “Obiettivi”- Costruire form con validazione
- Usare Alert per messaggi e conferme
- Salvare dati persistenti con AsyncStorage
- Creare una Todo List completa e persistente
1. Form con validazione
Section titled “1. Form con validazione”Pattern: stato per ogni campo + stato errore
Section titled “Pattern: stato per ogni campo + stato errore”const [email, setEmail] = useState('');const [password, setPassword] = useState('');const [errori, setErrori] = useState({});
const valida = () => { const nuoviErrori = {};
if (!email.trim()) { nuoviErrori.email = 'L\'email è obbligatoria'; } else if (!email.includes('@')) { nuoviErrori.email = 'Email non valida'; }
if (!password) { nuoviErrori.password = 'La password è obbligatoria'; } else if (password.length < 6) { nuoviErrori.password = 'Minimo 6 caratteri'; }
setErrori(nuoviErrori); return Object.keys(nuoviErrori).length === 0;};
const submit = () => { if (valida()) { Alert.alert('✅ Successo', 'Login valido!'); }};Mostrare errori nell’UI
Section titled “Mostrare errori nell’UI”<TextInput style={[ styles.input, errori.email && styles.inputErrore // ← bordo rosso ]} placeholder="Email" value={email} onChangeText={(text) => { setEmail(text); setErrori(prev => ({ ...prev, email: '' })); // pulisci errore }} keyboardType="email-address"/>{errori.email && ( <Text style={styles.erroreTesto}>{errori.email}</Text>)}const styles = StyleSheet.create({ input: { borderWidth: 1, borderColor: '#ddd', padding: 12, borderRadius: 8, marginBottom: 4, fontSize: 16, }, inputErrore: { borderColor: '#e74c3c', borderWidth: 2, }, erroreTesto: { color: '#e74c3c', fontSize: 12, marginBottom: 10, marginLeft: 4, },});2. Alert per scelte e conferme
Section titled “2. Alert per scelte e conferme”// Alert sempliceAlert.alert('Messaggio', 'Ciao mondo!');
// Alert con pulsanteAlert.alert('Successo', 'Operazione completata', [ { text: 'OK' }]);
// Alert con sceltaAlert.alert( 'Elimina', 'Sei sicuro?', [ { text: 'Annulla', style: 'cancel' }, { text: 'Elimina', onPress: () => elimina(), style: 'destructive' }, ]);Pattern: conferma prima di azione distruttiva
Section titled “Pattern: conferma prima di azione distruttiva”const eliminaTutto = () => { Alert.alert( '⚠️ Attenzione', 'Eliminare tutti i dati? Operazione irreversibile.', [ { text: 'Annulla', style: 'cancel' }, { text: 'Elimina tutto', style: 'destructive', onPress: () => { setDati([]); salvaDati([]); } }, ] );};3. AsyncStorage — persistenza dei dati
Section titled “3. AsyncStorage — persistenza dei dati”AsyncStorage salva dati in modo persistente sul telefono (sopravvive alla chiusura dell’app).
npx expo install @react-native-async-storage/async-storageFunzioni base
Section titled “Funzioni base”import AsyncStorage from '@react-native-async-storage/async-storage';
// Salvare (solo stringhe! usa JSON.stringify)const salva = async (chiave, valore) => { try { const json = JSON.stringify(valore); await AsyncStorage.setItem(chiave, json); } catch (e) { console.error('Errore salvataggio:', e); }};
// Caricareconst carica = async (chiave) => { try { const json = await AsyncStorage.getItem(chiave); return json ? JSON.parse(json) : null; } catch (e) { console.error('Errore caricamento:', e); return null; }};
// Eliminareconst eliminaChiave = async (chiave) => { try { await AsyncStorage.removeItem(chiave); } catch (e) { console.error('Errore eliminazione:', e); }};Pattern: stato iniziale da AsyncStorage
Section titled “Pattern: stato iniziale da AsyncStorage”const CHIAVE_LISTA = '@lista_spesa';
export default function App() { const [items, setItems] = useState([]); const [caricato, setCaricato] = useState(false);
// All'avvio: carica dati salvati useEffect(() => { const init = async () => { const salvati = await AsyncStorage.getItem(CHIAVE_LISTA); if (salvati) setItems(JSON.parse(salvati)); setCaricato(true); }; init(); }, []);
// A ogni modifica: salva useEffect(() => { if (caricato) { AsyncStorage.setItem(CHIAVE_LISTA, JSON.stringify(items)); } }, [items, caricato]);
// ...}4. Todo List completa e persistente
Section titled “4. Todo List completa e persistente”import { View, Text, TextInput, TouchableOpacity, FlatList, Alert, StyleSheet } from 'react-native';import { useState, useEffect } from 'react';import AsyncStorage from '@react-native-async-storage/async-storage';
const CHIAVE = '@todo_list';
export default function App() { const [task, setTask] = useState(''); const [tasks, setTasks] = useState([]); const [pronto, setPronto] = useState(false);
// Carica all'avvio useEffect(() => { const init = async () => { const salvate = await AsyncStorage.getItem(CHIAVE); if (salvate) setTasks(JSON.parse(salvate)); setPronto(true); }; init(); }, []);
// Salva a ogni modifica useEffect(() => { if (pronto) { AsyncStorage.setItem(CHIAVE, JSON.stringify(tasks)); } }, [tasks, pronto]);
const aggiungi = () => { if (!task.trim()) return; setTasks([ ...tasks, { id: Date.now().toString(), testo: task, fatto: false } ]); setTask(''); };
const toggleTask = (id) => { setTasks(tasks.map(t => t.id === id ? { ...t, fatto: !t.fatto } : t )); };
const eliminaTask = (id) => { setTasks(tasks.filter(t => t.id !== id)); };
const confermaElimina = (item) => { Alert.alert( 'Elimina', `Eliminare "${item.testo}"?`, [ { text: 'Annulla', style: 'cancel' }, { text: 'Elimina', onPress: () => eliminaTask(item.id), style: 'destructive' }, ] ); };
const renderItem = ({ item }) => ( <TouchableOpacity style={styles.taskItem} onPress={() => toggleTask(item.id)} onLongPress={() => confermaElimina(item)} > <View style={[styles.check, item.fatto && styles.checkFatto]}> {item.fatto && <Text style={styles.checkSegno}>✓</Text>} </View> <Text style={[styles.taskTesto, item.fatto && styles.taskFatto]}> {item.testo} </Text> <TouchableOpacity onPress={() => confermaElimina(item)}> <Text style={styles.eliminaIcona}>✕</Text> </TouchableOpacity> </TouchableOpacity> );
return ( <View style={styles.container}> <Text style={styles.titolo}>✅ Todo List</Text> <Text style={styles.sottotitolo}> {tasks.filter(t => t.fatto).length}/{tasks.length} completate </Text>
<View style={styles.form}> <TextInput style={styles.input} placeholder="Nuovo task..." value={task} onChangeText={setTask} onSubmitEditing={aggiungi} /> <TouchableOpacity style={styles.btnAggiungi} onPress={aggiungi}> <Text style={styles.btnTesto}>+</Text> </TouchableOpacity> </View>
<FlatList data={tasks} renderItem={renderItem} keyExtractor={item => item.id} ListEmptyComponent={ <Text style={styles.vuoto}> {pronto ? 'Nessun task. Aggiungine uno!' : 'Caricamento...'} </Text> } /> </View> );}
const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 50, paddingHorizontal: 20, backgroundColor: '#f5f5f5', }, titolo: { fontSize: 28, fontWeight: 'bold', marginBottom: 5, }, sottotitolo: { fontSize: 14, color: 'grey', marginBottom: 20, }, form: { flexDirection: 'row', gap: 10, marginBottom: 20, }, input: { flex: 1, backgroundColor: 'white', padding: 15, borderRadius: 10, fontSize: 16, elevation: 1, }, btnAggiungi: { width: 50, backgroundColor: '#3498db', borderRadius: 10, justifyContent: 'center', alignItems: 'center', }, btnTesto: { color: 'white', fontSize: 24, }, taskItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', padding: 15, borderRadius: 10, marginBottom: 8, elevation: 1, }, check: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: '#ccc', justifyContent: 'center', alignItems: 'center', marginRight: 12, }, checkFatto: { backgroundColor: '#2ecc71', borderColor: '#2ecc71', }, checkSegno: { color: 'white', fontSize: 14, fontWeight: 'bold', }, taskTesto: { flex: 1, fontSize: 16, }, taskFatto: { textDecorationLine: 'line-through', color: '#aaa', }, eliminaIcona: { fontSize: 18, color: '#e74c3c', padding: 5, }, vuoto: { textAlign: 'center', color: 'grey', marginTop: 50, fontSize: 16, },});5. Pattern: reset e cancellazione dati
Section titled “5. Pattern: reset e cancellazione dati”const resetDati = async () => { Alert.alert( 'Reset', 'Cancellare tutti i dati salvati?', [ { text: 'Annulla', style: 'cancel' }, { text: 'Reset', style: 'destructive', onPress: async () => { await AsyncStorage.clear(); // Cancella TUTTO setTasks([]); setPronto(false); // Ricarica stato iniziale setPronto(true); } }, ] );};6. Tabella riassuntiva
Section titled “6. Tabella riassuntiva”| Operazione | Codice |
|---|---|
| Salvare dati | AsyncStorage.setItem(key, JSON.stringify(val)) |
| Caricare dati | JSON.parse(await AsyncStorage.getItem(key)) |
| Eliminare chiave | AsyncStorage.removeItem(key) |
| Cancellare tutto | AsyncStorage.clear() |
| Alert informativo | Alert.alert('Titolo', 'Msg') |
| Alert con scelta | Alert.alert('Titolo', 'Msg', [{text:'Si', onPress:fn}]) |
7. Esercizio autonomo
Section titled “7. Esercizio autonomo”🎯 Esercizio: “Diario personale”
Section titled “🎯 Esercizio: “Diario personale””Crea un’app diario che:
- Form per scrivere: titolo + testo + pulsante “Salva”
- Data automatica:
new Date().toLocaleDateString('it-IT') - AsyncStorage per salvare tutti i post
- FlatList per mostrare i post salvati
- TouchableOpacity per leggere il post completo (Alert)
- Long-press per eliminare
- Messaggio “Nessun post” quando vuoto
Dati tipo
Section titled “Dati tipo”{ id: '12345', titolo: 'Primo giorno', testo: '...', data: '27/06/2026' }