Lesson 50 minNEW

Project 3 — Weather API App

00:00 / 00:00

Project 3 — Weather API App

In this project we go online for the first time! We'll build a CLI weather app that fetches real, live weather data from the internet using Dart's http package. You'll learn how to make HTTP requests, parse JSON responses, and handle network errors gracefully.


🎯 What We're Building

A command-line weather app that:

  • Accepts a city name from the user
  • Uses the Open-Meteo API (100% free, no API key required)
  • Uses the Geocoding API to convert city → coordinates
  • Displays temperature, wind speed, and weather conditions
  • Handles network errors and invalid city names

🌐 API Overview

We'll use two free APIs:

APIPurposeURL
Open-Meteo GeocodingCity name → latitude/longitudegeocoding-api.open-meteo.com
Open-Meteo WeatherCoordinates → weather dataapi.open-meteo.com

Why Open-Meteo? It's completely free, requires no API key, has no rate limits for reasonable usage, and returns clean JSON. Perfect for learning!


🧠 Concepts Used

  • ✅ HTTP requests with the http package
  • async / await and Future
  • ✅ JSON parsing with dart:convert
  • ✅ Model classes with fromJson constructors
  • ✅ Exception handling for network failures
  • Uri.https() for building safe URLs
  • ✅ Enums for weather condition codes

📁 Project Structure

weather_app/
├── bin/
│   └── main.dart             ← Entry point & CLI
├── lib/
│   ├── models/
│   │   ├── location.dart     ← Location/city model
│   │   └── weather_data.dart ← Weather model
│   └── services/
│       └── weather_service.dart ← API calls
└── pubspec.yaml

Step 1 — Set Up the Project

bash
dart create weather_app cd weather_app

Add the http package to pubspec.yaml:

yaml
dependencies: http: ^1.2.0

Then fetch it:

bash
dart pub get

Step 2 — The Location Model

Create lib/models/location.dart:

dart
// lib/models/location.dart class Location { final String name; // City name as returned by the API final String country; // Country code (e.g., "US", "IN") final double latitude; final double longitude; Location({ required this.name, required this.country, required this.latitude, required this.longitude, }); // Parse from the Open-Meteo Geocoding API response factory Location.fromJson(Map<String, dynamic> json) { return Location( name: json['name'] as String, country: json['country'] as String? ?? 'Unknown', latitude: (json['latitude'] as num).toDouble(), longitude: (json['longitude'] as num).toDouble(), ); } String toString() => '$name, $country ($latitude, $longitude)'; }

Step 3 — The WeatherData Model

Create lib/models/weather_data.dart:

dart
// lib/models/weather_data.dart class WeatherData { final double temperatureCelsius; final double feelsLikeCelsius; final double windSpeedKmh; final int weatherCode; // WMO weather interpretation code final String condition; // Human-readable description final String emoji; // Visual indicator WeatherData({ required this.temperatureCelsius, required this.feelsLikeCelsius, required this.windSpeedKmh, required this.weatherCode, }) : condition = _codeToCondition(weatherCode), emoji = _codeToEmoji(weatherCode); // Parse from the Open-Meteo weather API response factory WeatherData.fromJson(Map<String, dynamic> json) { final current = json['current'] as Map<String, dynamic>; return WeatherData( temperatureCelsius: (current['temperature_2m'] as num).toDouble(), feelsLikeCelsius: (current['apparent_temperature'] as num).toDouble(), windSpeedKmh: (current['wind_speed_10m'] as num).toDouble(), weatherCode: current['weather_code'] as int, ); } double get temperatureFahrenheit => (temperatureCelsius * 9 / 5) + 32; // ─── WMO Weather Code Interpretation ───────────────────────── // Full code table: https://open-meteo.com/en/docs static String _codeToCondition(int code) { return switch (code) { 0 => 'Clear Sky', 1 => 'Mainly Clear', 2 => 'Partly Cloudy', 3 => 'Overcast', 45 || 48 => 'Foggy', 51 || 53 || 55 => 'Drizzle', 61 || 63 || 65 => 'Rain', 71 || 73 || 75 => 'Snow', 80 || 81 || 82 => 'Rain Showers', 95 => 'Thunderstorm', 96 || 99 => 'Thunderstorm with Hail', _ => 'Unknown', }; } static String _codeToEmoji(int code) { return switch (code) { 0 => '☀️', 1 || 2 => '🌤️', 3 => '☁️', 45 || 48 => '🌫️', 51 || 53 || 55 => '🌦️', 61 || 63 || 65 => '🌧️', 71 || 73 || 75 => '❄️', 80 || 81 || 82 => '🌦️', 95 || 96 || 99 => '⛈️', _ => '🌡️', }; } }

💡 Pattern Matching: Notice the switch expression with the || pattern — this is Dart 3's pattern matching. case 1 || 2 => ... matches either 1 or 2. Very clean!


Step 4 — The WeatherService Class

Create lib/services/weather_service.dart:

dart
// lib/services/weather_service.dart import 'dart:convert'; import 'package:http/http.dart' as http; import '../models/location.dart'; import '../models/weather_data.dart'; class WeatherService { // Base hosts static const _geoHost = 'geocoding-api.open-meteo.com'; static const _weatherHost = 'api.open-meteo.com'; // ─── PUBLIC API ─────────────────────────────────────────────── /// High-level method: takes a city name, returns WeatherData. /// Throws [WeatherException] on any error. Future<({Location location, WeatherData weather})> fetchWeather( String cityName) async { final location = await _geocodeCity(cityName); final weather = await _fetchWeatherForLocation(location); return (location: location, weather: weather); } // ─── PRIVATE HELPERS ────────────────────────────────────────── /// Step 1: Convert city name → lat/lon using Geocoding API Future<Location> _geocodeCity(String cityName) async { final uri = Uri.https( _geoHost, '/v1/search', { 'name': cityName, 'count': '1', // We only need the top result 'language': 'en', 'format': 'json', }, ); final response = await _get(uri); final json = jsonDecode(response.body) as Map<String, dynamic>; final results = json['results'] as List<dynamic>?; if (results == null || results.isEmpty) { throw WeatherException('City "$cityName" not found. ' 'Try a different spelling or a nearby city.'); } return Location.fromJson(results.first as Map<String, dynamic>); } /// Step 2: Fetch weather for given coordinates Future<WeatherData> _fetchWeatherForLocation( Location location) async { final uri = Uri.https( _weatherHost, '/v1/forecast', { 'latitude': location.latitude.toString(), 'longitude': location.longitude.toString(), 'current': [ 'temperature_2m', 'apparent_temperature', 'weather_code', 'wind_speed_10m', ].join(','), 'timezone': 'auto', }, ); final response = await _get(uri); final json = jsonDecode(response.body) as Map<String, dynamic>; return WeatherData.fromJson(json); } /// Shared GET request with timeout and status code checking Future<http.Response> _get(Uri uri) async { try { final response = await http .get(uri) .timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { throw WeatherException( 'API error: HTTP ${response.statusCode}'); } return response; } on http.ClientException catch (e) { throw WeatherException( 'Network error: ${e.message}. ' 'Please check your internet connection.'); } on Exception catch (e) { // Catches TimeoutException and other unexpected errors throw WeatherException('Unexpected error: $e'); } } } /// Custom exception for weather-related errors class WeatherException implements Exception { final String message; WeatherException(this.message); String toString() => message; }

💡 Named Records: Future<({Location location, WeatherData weather})> uses Dart 3 named records as a return type. This is a clean way to return multiple values without creating a new class!


Step 5 — The CLI Entry Point

Create bin/main.dart:

dart
// bin/main.dart import 'dart:io'; import '../lib/models/weather_data.dart'; import '../lib/models/location.dart'; import '../lib/services/weather_service.dart'; final _service = WeatherService(); Future<void> main() async { printBanner(); while (true) { final cityInput = prompt('\n🏙️ Enter city name (or "quit" to exit)'); if (cityInput == null || cityInput.trim().isEmpty) { print('⚠️ Please enter a city name.'); continue; } final city = cityInput.trim(); if (city.toLowerCase() == 'quit' || city.toLowerCase() == 'exit') { print('\n👋 Stay warm out there!\n'); exit(0); } await getAndDisplayWeather(city); } } Future<void> getAndDisplayWeather(String city) async { print('\n⏳ Fetching weather for "$city"...'); try { final result = await _service.fetchWeather(city); displayWeather(result.location, result.weather); } on WeatherException catch (e) { // Our custom exception — friendly message print('\n❌ $e'); } catch (e) { // Truly unexpected error print('\n❌ Something went wrong: $e'); } } void displayWeather(Location location, WeatherData weather) { final tempC = weather.temperatureCelsius.toStringAsFixed(1); final tempF = weather.temperatureFahrenheit.toStringAsFixed(1); final feelsC = weather.feelsLikeCelsius.toStringAsFixed(1); final wind = weather.windSpeedKmh.toStringAsFixed(1); print(''' ╔════════════════════════════════════════╗ ${weather.emoji} Weather in ${location.name}, ${location.country} ╠════════════════════════════════════════╣ ║ 🌡️ Temperature: ${tempC}°C / ${tempF}°F ║ 🤔 Feels Like: ${feelsC}°C ║ 💨 Wind Speed: ${wind} km/h ║ ☁️ Condition: ${weather.condition} ╚════════════════════════════════════════╝ '''); } String? prompt(String message) { stdout.write(message + ': '); return stdin.readLineSync(); } void printBanner() { print(''' ╔════════════════════════════════════════╗ ║ ⛅ DART WEATHER CLI APP ║ ║ Powered by Open-Meteo (free!) ║ ╚════════════════════════════════════════╝'''); }

Step 6 — Run the App

bash
dart run bin/main.dart

Sample session:

⛅  DART WEATHER CLI APP
Powered by Open-Meteo (free!)

🏙️  Enter city name (or "quit" to exit): London

⏳ Fetching weather for "London"...

╔════════════════════════════════════════╗
║  🌧️  Weather in London, GB
╠════════════════════════════════════════╣
║  🌡️  Temperature:   12.4°C / 54.3°F
║  🤔  Feels Like:    9.1°C
║  💨  Wind Speed:    23.5 km/h
║  ☁️  Condition:     Rain
╚════════════════════════════════════════╝

🔍 Understanding the HTTP Flow

User types "Mumbai"
       │
       ▼
1. Geocoding API
   GET geocoding-api.open-meteo.com/v1/search?name=Mumbai
   ← Returns: { "results": [{ "latitude": 19.07, "longitude": 72.87, ... }] }
       │
       ▼
2. Weather API
   GET api.open-meteo.com/v1/forecast?latitude=19.07&longitude=72.87&current=...
   ← Returns: { "current": { "temperature_2m": 31.2, "weather_code": 0, ... } }
       │
       ▼
3. Display formatted output to user

🛡️ Error Handling Strategy

Error TypeHow We Handle It
City not foundWeatherException with friendly message
No internetClientExceptionWeatherException
API timeoutTimeoutException caught → WeatherException
Bad HTTP statusCheck statusCode != 200
Unexpected errorsGeneric catch (e) as last resort

🔥 Bonus: 7-Day Forecast

To add a 7-day forecast, add daily parameters to your request:

dart
// In _fetchWeatherForLocation(), add to query params: 'daily': 'temperature_2m_max,temperature_2m_min,weather_code', // Parse in WeatherData.fromJson(): final daily = json['daily'] as Map<String, dynamic>; final maxTemps = (daily['temperature_2m_max'] as List) .map((t) => (t as num).toDouble()) .toList();

💡 Key Takeaways

ConceptWhere Used
http.get()Fetching data from Open-Meteo
jsonDecode()Parsing the API response
Uri.https()Building safe, encoded URLs
async/awaitAll network calls
Future<T>Return type of async methods
Named records ({...})Returning multiple values
switch expressionsWMO code → condition/emoji
Custom exceptionsWeatherException
.timeout()Preventing infinite hangs

Summary

You just built a real API-powered application in Dart! This project demonstrated:

  • HTTP requests using the http package
  • Chaining async calls — geocoding first, then weather
  • JSON parsing into model classes
  • Layered error handling from network to display
  • Pattern matching with switch expressions and || patterns

This is a major milestone — you can now connect any Dart app to the internet. Next up: we'll create and publish a real Dart package on pub.dev!

WhatsApp