📡 Project: Weather CLI Tool
📚 Lesson 11: Fetch Live Data
⏱ 35-40 min
📊 Advanced
🎯 Objective
Build a complete weather CLI tool that fetches and displays weather data. Practice API integration, caching, command-line argument parsing, and formatted output display.
Requirements
- Create a
WeatherServiceclass that simulates API calls - Support commands:
current,forecast,compare - Implement a simple cache to avoid repeated API calls
- Display formatted weather with emoji indicators
- Support multiple cities comparison
- Handle API errors and timeouts gracefully
Expected Output
Terminal
$ dart run weather.dart current London
🌤️ Weather for London, GB
─────────────────────────────────────
🌡️ Temperature: 15°C (Feels like: 13°C)
💧 Humidity: 78%
🌬️ Wind: 12 km/h SW
📋 Conditions: Rainy
─────────────────────────────────────
💡 Tip: Bring an umbrella!
$ dart run weather.dart forecast Tokyo
📅 5-Day Forecast for Tokyo, JP
─────────────────────────────────────
Mon: ☀️ 28°C | Sunny
Tue: ⛅ 26°C | Partly Cloudy
Wed: 🌧️ 22°C | Rainy
Thu: ☀️ 29°C | Clear Sky
Fri: ⛅ 27°C | Partly Cloudy
$ dart run weather.dart compare London Tokyo Dubai
📊 Weather Comparison
─────────────────────────────────────
🌤️ London | 15°C | Rainy
☀️ Tokyo | 28°C | Sunny
🔥 Dubai | 38°C | Hot
─────────────────────────────────────
🔥 Hottest: Dubai (38°C)
❄️ Coldest: London (15°C)
import 'dart:async';
import 'dart:math';
// Step 1: Weather data models
class WeatherData {
final String city;
final String country;
final double temperature;
final double feelsLike;
final int humidity;
final double windSpeed;
final String windDirection;
final String condition;
final String icon;
final DateTime timestamp;
WeatherData({
required this.city,
required this.country,
required this.temperature,
required this.feelsLike,
required this.humidity,
required this.windSpeed,
required this.windDirection,
required this.condition,
required this.icon,
required this.timestamp,
});
String get tempDisplay => '${temperature.toStringAsFixed(0)}°C';
String get feelsLikeDisplay => '${feelsLike.toStringAsFixed(0)}°C';
void display() {
print('$icon Weather for $city, $country');
print('─' * 45);
print('🌡️ Temperature: $tempDisplay (Feels like: $feelsLikeDisplay)');
print('💧 Humidity: $humidity%');
print('🌬️ Wind: ${windSpeed.toStringAsFixed(0)} km/h $windDirection');
print('📋 Conditions: $condition');
print('─' * 45);
// Tips based on conditions
if (condition == 'Rainy' || condition == 'Thunderstorm') {
print('💡 Tip: Bring an umbrella!');
} else if (temperature > 35) {
print('💡 Tip: Stay hydrated and avoid direct sun!');
} else if (temperature < 5) {
print('💡 Tip: Dress warmly and watch for ice!');
}
}
}
class ForecastDay {
final String day;
final double highTemp;
final double lowTemp;
final String condition;
final String icon;
ForecastDay({
required this.day,
required this.highTemp,
required this.lowTemp,
required this.condition,
required this.icon,
});
void display() {
print('$day: $icon ${highTemp.toStringAsFixed(0)}°C / ${lowTemp.toStringAsFixed(0)}°C | $condition');
}
}
// Step 2: Custom exceptions
class WeatherApiException implements Exception {
final String message;
WeatherApiException(this.message);
@override
String toString() => 'Weather API Error: $message';
}
class CityNotFoundException extends WeatherApiException {
CityNotFoundException(String city) : super('City not found: $city');
}
// Step 3: Weather service with caching
class WeatherService {
final Random _random = Random();
final Map _cache = {};
final Map _cacheTimestamps = {};
static const Duration _cacheDuration = Duration(minutes: 10);
bool _isCacheValid(String city) {
if (!_cacheTimestamps.containsKey(city)) return false;
return DateTime.now().difference(_cacheTimestamps[city]!) < _cacheDuration;
}
// Simulated city database
static const _cityDatabase = {
'london': {'country': 'GB', 'baseTemp': 14, 'baseHumidity': 75},
'tokyo': {'country': 'JP', 'baseTemp': 22, 'baseHumidity': 55},
'dubai': {'country': 'AE', 'baseTemp': 36, 'baseHumidity': 25},
'new york': {'country': 'US', 'baseTemp': 18, 'baseHumidity': 60},
'sydney': {'country': 'AU', 'baseTemp': 24, 'baseHumidity': 50},
'moscow': {'country': 'RU', 'baseTemp': 8, 'baseHumidity': 70},
'mumbai': {'country': 'IN', 'baseTemp': 32, 'baseHumidity': 65},
'paris': {'country': 'FR', 'baseTemp': 16, 'baseHumidity': 68},
};
Future getCurrentWeather(String city) async {
// Check cache first
if (_isCacheValid(city.toLowerCase())) {
print('📦 Using cached data for $city');
return _cache[city.toLowerCase()]!;
}
// Simulate API delay
await Future.delayed(Duration(milliseconds: 300 + _random.nextInt(500)));
// Validate city
final cityData = _cityDatabase[city.toLowerCase()];
if (cityData == null) {
throw CityNotFoundException(city);
}
// Simulate occasional server error (5% chance)
if (_random.nextInt(100) < 5) {
throw WeatherApiException('Service temporarily unavailable');
}
// Generate weather data
final conditions = ['Sunny', 'Partly Cloudy', 'Cloudy', 'Rainy', 'Clear Sky', 'Windy', 'Hot', 'Mild'];
final icons = {'Sunny': '☀️', 'Partly Cloudy': '⛅', 'Cloudy': '☁️', 'Rainy': '🌧️', 'Clear Sky': '☀️', 'Windy': '💨', 'Hot': '🔥', 'Mild': '🌤️'};
final condition = conditions[_random.nextInt(conditions.length)];
final tempVariation = _random.nextDouble() * 8 - 3;
final temperature = cityData['baseTemp'] as int + tempVariation;
final weather = WeatherData(
city: city[0].toUpperCase() + city.substring(1),
country: cityData['country'] as String,
temperature: temperature,
feelsLike: temperature - _random.nextDouble() * 3,
humidity: (cityData['baseHumidity'] as int) + _random.nextInt(15) - 7,
windSpeed: 5 + _random.nextDouble() * 20,
windDirection: ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][_random.nextInt(8)],
condition: condition,
icon: icons[condition] ?? '🌤️',
timestamp: DateTime.now(),
);
// Cache the result
_cache[city.toLowerCase()] = weather;
_cacheTimestamps[city.toLowerCase()] = DateTime.now();
return weather;
}
Future
- > getForecast(String city) async {
await Future.delayed(Duration(milliseconds: 200));
final cityData = _cityDatabase[city.toLowerCase()];
if (cityData == null) throw CityNotFoundException(city);
final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
final conditions = ['Sunny', 'Partly Cloudy', 'Cloudy', 'Rainy', 'Clear Sky', 'Windy'];
final icons = {'Sunny': '☀️', 'Partly Cloudy': '⛅', 'Cloudy': '☁️', 'Rainy': '🌧️', 'Clear Sky': '☀️', 'Windy': '💨'};
return days.map((day) {
final condition = conditions[_random.nextInt(conditions.length)];
final baseTemp = cityData['baseTemp'] as int;
return ForecastDay(
day: day,
highTemp: baseTemp + _random.nextDouble() * 6,
lowTemp: baseTemp - _random.nextDouble() * 4,
condition: condition,
icon: icons[condition] ?? '🌤️',
);
}).toList();
}
void clearCache() {
_cache.clear();
_cacheTimestamps.clear();
print('🗑️ Cache cleared');
}
}
// Step 4: CLI handler
class WeatherCLI {
final WeatherService _service = WeatherService();
Future