Lezione 11: Esercitazione guidata — App Meteo
Obiettivi
Section titled “Obiettivi”- Mettere insieme API, navigazione e UI
- Gestire lo stato di caricamento
- Visualizzare dati strutturati
- Completare un’app funzionante dall’inizio alla fine
1. Panoramica del progetto
Section titled “1. Panoramica del progetto”Costruiamo un’app che:
- Chiede il nome di una città
- Chiama un’API meteo per ottenere i dati
- Mostra temperatura, umidità, descrizione del tempo
- Permette di cercare un’altra città
API che useremo
Section titled “API che useremo”Open-Meteo (gratuita, senza API key):
https://api.open-meteo.com/v1/forecast?latitude=LAT&longitude=LON¤t_units=temperature_2m¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code&timezone=autoMa per semplificare, prima cerchiamo le coordinate della città con Geocoding API (gratuita, senza API key):
https://geocoding-api.open-meteo.com/v1/search?name=NOME_CITTÀ2. Step 1: Setup e ricerca città
Section titled “2. Step 1: Setup e ricerca città”import flet as ftimport urllib.requestimport json
def main(page: ft.Page): page.title = "Meteo App" page.padding = 30 page.scroll = ft.ScrollMode.AUTO
# Widget UI input_citta = ft.TextField( label="Città", hint_text="es. Roma, Milano, Parigi", prefix_icon=ft.icons.SEARCH, on_submit=lambda e: cerca_meteo(e), ) bottone_cerca = ft.ElevatedButton("Cerca", on_click=cerca_meteo) caricamento = ft.ProgressBar(visible=False) errore = ft.Text("", color="red") risultato = ft.Column()
page.add( ft.Text("🌤️ Meteo App", size=28, weight="bold"), ft.Row([input_citta, bottone_cerca]), errore, caricamento, risultato, )
# Funzioni (da implementare) def cerca_meteo(e): pass
ft.app(target=main)3. Step 2: Funzione di ricerca coordinate
Section titled “3. Step 2: Funzione di ricerca coordinate”def cerca_coordinate(citta): """Cerca le coordinate di una città usando Geocoding API.""" url = f"https://geocoding-api.open-meteo.com/v1/search?name={citta}&count=1" with urllib.request.urlopen(url) as response: dati = json.loads(response.read().decode())
if dati.get("results"): risultato = dati["results"][0] return { "nome": risultato["name"], "lat": risultato["latitude"], "lon": risultato["longitude"], "paese": risultato.get("country", ""), } return None4. Step 3: Funzione di recupero meteo
Section titled “4. Step 3: Funzione di recupero meteo”def cerca_meteo_dati(lat, lon): """Recupera i dati meteo per coordinate.""" url = ( f"https://api.open-meteo.com/v1/forecast?" f"latitude={lat}&longitude={lon}" f"¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code" f"&timezone=auto" ) with urllib.request.urlopen(url) as response: return json.loads(response.read().decode())5. Step 4: Decodifica codice meteo
Section titled “5. Step 4: Decodifica codice meteo”Open-Meteo usa codici numerici per il tempo. Creiamo una funzione per convertirli:
def decodifica_meteo(codice): """Converte il codice WMO in descrizione testuale.""" meteo = { 0: "☀️ Sereno", 1: "🌤️ Prevalentemente sereno", 2: "⛅ Parzialmente nuvoloso", 3: "☁️ Coperto", 45: "🌫️ Nebbia", 48: "🌫️ Nebbia con ghiaccio", 51: "🌧️ Pioggerella leggera", 53: "🌧️ Pioggerella moderata", 55: "🌧️ Pioggerella intensa", 61: "🌦️ Pioggia leggera", 63: "🌧️ Pioggia moderata", 65: "🌧️ Pioggia intensa", 71: "🌨️ Neve leggera", 73: "🌨️ Neve moderata", 75: "🌨️ Neve intensa", 80: "🌦️ Rovesci leggeri", 81: "🌧️ Rovesci moderati", 82: "🌧️ Rovesci intensi", 95: "⛈️ Temporale", 96: "⛈️ Grandine leggera", 99: "⛈️ Grandine forte", } return meteo.get(codice, f"❓ Codice {codice}")6. Step 5: Funzione principale — cerca meteo
Section titled “6. Step 5: Funzione principale — cerca meteo”def cerca_meteo(e): if not input_citta.value.strip(): return
# Reset UI errore.value = "" risultato.controls.clear() caricamento.visible = True page.update()
try: # Cerca coordinate città luogo = cerca_coordinate(input_citta.value.strip())
if not luogo: errore.value = f"❌ Città '{input_citta.value}' non trovata!" caricamento.visible = False page.update() return
# Recupera meteo meteo = cerca_meteo_dati(luogo["lat"], luogo["lon"]) corrente = meteo["current"]
# Estrai dati temperatura = corrente["temperature_2m"] percepita = corrente["apparent_temperature"] umidita = corrente["relative_humidity_2m"] codice_meteo = corrente["weather_code"] descrizione = decodifica_meteo(codice_meteo)
# Costruisci UI risultato risultato.controls.append( ft.Container( content=ft.Column([ # Città e paese ft.Text(f"{luogo['nome']}, {luogo['paese']}", size=24, weight="bold"), ft.Divider(),
# Icona meteo e temperatura grande ft.Text(descrizione, size=22), ft.Text(f"{temperatura}°C", size=60, weight="bold", color="blue"),
ft.Divider(),
# Dettagli ft.Row([ ft.Column([ ft.Text("🌡️ Percepita", color="grey"), ft.Text(f"{percepita}°C", size=20, weight="bold"), ], horizontal_alignment=ft.CrossAxisAlignment.CENTER), ft.Column([ ft.Text("💧 Umidità", color="grey"), ft.Text(f"{umidita}%", size=20, weight="bold"), ], horizontal_alignment=ft.CrossAxisAlignment.CENTER), ], alignment=ft.MainAxisAlignment.SPACE_AROUND), ]), padding=25, bgcolor="white", border_radius=20, shadow=ft.BoxShadow(blur_radius=15, color=ft.colors.GREY_300), width=350, ) )
except Exception as ex: errore.value = f"❌ Errore: {ex}" finally: caricamento.visible = False page.update()7. Codice completo dell’app
Section titled “7. Codice completo dell’app”import flet as ftimport urllib.requestimport json
WEATHER_CODES = { 0: "☀️ Sereno", 1: "🌤️ Prevalentemente sereno", 2: "⛅ Parzialmente nuvoloso", 3: "☁️ Coperto", 45: "🌫️ Nebbia", 48: "🌫️ Nebbia con ghiaccio", 51: "🌧️ Pioggerella leggera", 53: "🌧️ Pioggerella moderata", 55: "🌧️ Pioggerella intensa", 61: "🌦️ Pioggia leggera", 63: "🌧️ Pioggia moderata", 65: "🌧️ Pioggia intensa", 71: "🌨️ Neve leggera", 73: "🌨️ Neve moderata", 75: "🌨️ Neve intensa", 80: "🌦️ Rovesci leggeri", 81: "🌧️ Rovesci moderati", 82: "🌧️ Rovesci intensi", 95: "⛈️ Temporale", 96: "⛈️ Grandine leggera", 99: "⛈️ Grandine forte",}
def decodifica_meteo(codice): return WEATHER_CODES.get(codice, f"❓ Codice {codice}")
def cerca_coordinate(citta): url = f"https://geocoding-api.open-meteo.com/v1/search?name={citta}&count=1" with urllib.request.urlopen(url) as response: dati = json.loads(response.read().decode()) if dati.get("results"): r = dati["results"][0] return {"nome": r["name"], "lat": r["latitude"], "lon": r["longitude"], "paese": r.get("country", "")} return None
def cerca_meteo_dati(lat, lon): url = (f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" f"¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code" f"&timezone=auto") with urllib.request.urlopen(url) as response: return json.loads(response.read().decode())
def main(page: ft.Page): page.title = "🌤️ Meteo App" page.padding = 30 page.scroll = ft.ScrollMode.AUTO
input_citta = ft.TextField(label="Città", hint_text="es. Roma, Milano, Parigi", prefix_icon=ft.icons.SEARCH, on_submit=lambda e: cerca_meteo(e)) bottone_cerca = ft.ElevatedButton("Cerca", on_click=cerca_meteo) caricamento = ft.ProgressBar(visible=False) errore = ft.Text("", color="red") risultato = ft.Column()
page.add( ft.Text("🌤️ Meteo App", size=28, weight="bold"), ft.Text("Controlla il meteo in qualsiasi città", color="grey"), ft.Divider(), ft.Row([input_citta, bottone_cerca]), errore, caricamento, risultato, )
def cerca_meteo(e): if not input_citta.value.strip(): return errore.value = "" risultato.controls.clear() caricamento.visible = True page.update()
try: luogo = cerca_coordinate(input_citta.value.strip()) if not luogo: errore.value = f"❌ Città '{input_citta.value}' non trovata!" return
meteo = cerca_meteo_dati(luogo["lat"], luogo["lon"]) c = meteo["current"]
risultato.controls.append( ft.Container( content=ft.Column([ ft.Text(f"{luogo['nome']}, {luogo['paese']}", size=24, weight="bold"), ft.Divider(), ft.Text(decodifica_meteo(c["weather_code"]), size=22), ft.Text(f"{c['temperature_2m']}°C", size=60, weight="bold", color="blue"), ft.Divider(), ft.Row([ ft.Column([ft.Text("🌡️ Percepita", color="grey"), ft.Text(f"{c['apparent_temperature']}°C", size=20, weight="bold")], horizontal_alignment=ft.CrossAxisAlignment.CENTER), ft.Column([ft.Text("💧 Umidità", color="grey"), ft.Text(f"{c['relative_humidity_2m']}%", size=20, weight="bold")], horizontal_alignment=ft.CrossAxisAlignment.CENTER), ], alignment=ft.MainAxisAlignment.SPACE_AROUND), ]), padding=25, bgcolor="white", border_radius=20, shadow=ft.BoxShadow(blur_radius=15, color=ft.colors.GREY_300), width=350, ) ) except Exception as ex: errore.value = f"❌ Errore: {ex}" finally: caricamento.visible = False page.update()
ft.app(target=main)8. Sfide extra (se hai tempo)
Section titled “8. Sfide extra (se hai tempo)”Dopo aver completato l’app base, prova ad aggiungere:
- Cronologia ricerche: salva le ultime città cercate (JSON)
- Preferiti: stella per salvare città preferite
- Icona meteo animata: usa emoji diverse in base al codice
- Previsioni 7 giorni: mostra anche i giorni successivi (usa
dailyinvece dicurrentnell’API) - Tema scuro per l’app
API per previsioni 7 giorni
Section titled “API per previsioni 7 giorni”url = ( f"https://api.open-meteo.com/v1/forecast?" f"latitude={lat}&longitude={lon}" f"&daily=temperature_2m_max,temperature_2m_min,weather_code" f"&timezone=auto")9. Esercizio autonomo
Section titled “9. Esercizio autonomo”🎯 Esercizio: “Personalizza la Meteo App”
Section titled “🎯 Esercizio: “Personalizza la Meteo App””Modifica l’app meteo per aggiungere:
- Colore di sfondo che cambia in base al tempo:
- Sereno → azzurro
- Nuvoloso → grigio
- Pioggia → blu scuro
- Salvataggio ultima città in un file JSON (così all’avvio mostra subito il meteo)
- Bottone “Aggiorna” per ricaricare i dati senza riscrivere la città
- Extra: se cerchi una città già vista, mostra direttamente i dati senza chiamare la geocoding API