In the previous lesson, we explored unit testing in depth, mastering methods to ensure code quality through comprehensive testing. Today, in Lesson 25, we'll tackle a practical project: building a weather data parser using core Dart features. We'll retrieve weather information from a public API, parse the JSON response, and demonstrate key concepts like HTTP requests, asynchronous programming, and null safety—all without needing an API key or registration.
I. Project Requirements & Preparation
1. Project Goals
We'll create a Dart application that:
- Fetches current weather data from a public API (no API key required)
- Parses JSON responses into structured Dart objects
- Handles potential errors (network issues, invalid data)
- Safely manages null values to prevent crashes
- Displays formatted weather information in the console
2. API Selection: Open-Meteo
We'll use the Open-Meteo API—a free, open-source weather API that doesn't require registration. It provides weather data based on geographic coordinates (latitude/longitude) and returns well-structured JSON responses.
Key endpoint: https://api.open-meteo.com/v1/forecast
Required parameters:
- latitude/longitude: Geographic coordinates
- current_weather: Set to true to get current conditions
II. Implementation Steps
1. Project Setup & Dependencies
Create a new Dart project and add the http package to pubspec.yaml:
dependencies:
http: ^1.4.0
Install dependencies:
dart pub get
2. Making HTTP Requests
Create a service class to handle API communication:
// lib/weather_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class WeatherService {
// Base URL for the weather API
static const String baseUrl = 'api.open-meteo.com';
// Fetch weather data using geographic coordinates
Future<Map<String, dynamic>> fetchWeather({
required double latitude,
required double longitude,
}) async {
// Construct the API URL with required parameters
final uri = Uri.https(baseUrl, '/v1/forecast', {
'latitude': latitude.toString(),
'longitude': longitude.toString(),
'current_weather': 'true',
'temperature_unit': 'celsius', // Get temperature in Celsius
});
try {
// Send HTTP GET request
final response = await http.get(uri);
// Check if request was successful
if (response.statusCode == 200) {
// Parse JSON response to Map
return json.decode(response.body) as Map<String, dynamic>;
} else {
throw Exception(
'Failed to fetch data. Status code: ${response.statusCode}',
);
}
} catch (e) {
// Handle network errors (e.g., no internet connection)
throw Exception('Network error: $e');
}
}
}
Key concepts:
- Uri.https safely constructs URLs with query parameters
- async/await handles asynchronous HTTP requests
- Try-catch blocks manage both network errors and HTTP status codes
3. Creating Data Models
Convert raw JSON data into type-safe Dart objects. The Open-Meteo response contains a current_weather object with key fields:
// lib/weather_model.dart
class Weather {
final double temperature; // In Celsius
final double windspeed; // In km/h
final int weathercode; // Code representing weather condition
final DateTime time; // Observation time
// Constructor with required parameters
Weather({
required this.temperature,
required this.windspeed,
required this.weathercode,
required this.time,
});
// Factory method to create Weather from JSON
factory Weather.fromJson(Map<String, dynamic> json) {
// Extract the nested current_weather object
final currentWeather = json['current_weather'] as Map<String, dynamic>;
return Weather(
temperature: (currentWeather['temperature'] as num).toDouble(),
windspeed: (currentWeather['windspeed'] as num).toDouble(),
weathercode: currentWeather['weathercode'] as int,
time: DateTime.parse(currentWeather['time'] as String),
);
}
// Get human-readable weather condition from code
String get weatherCondition {
switch (weathercode) {
case 0:
return 'Clear sky';
case 1:
return 'Partly cloudy';
default:
return 'Unknown';
}
}
}
Key features:
- Type conversion from num to double for numerical values
- DateTime parsing from ISO 8601 string
- weatherCondition getter converts numeric code to readable text
- Strict type casting with as operator
4. Null Safety Implementation
The Open-Meteo API is reliable, but we still need to handle potential missing fields:
// Enhanced null safety in fromJson
factory Weather.fromJson(Map<String, dynamic> json) {
// Check for required top-level field
if (!json.containsKey('current_weather')) {
throw FormatException('Missing "current_weather" in response');
}
final currentWeather = json['current_weather'] as Map<String, dynamic>?;
if (currentWeather == null) {
throw FormatException('"current_weather" cannot be null');
}
return Weather(
temperature: (currentWeather['temperature'] as num?)?.toDouble()?? 0.0,
windspeed: (currentWeather['windspeed'] as num?)?.toDouble()?? 0.0,
weathercode: currentWeather['weathercode'] as int? ?? -1,
time: currentWeather['time'] is String
? DateTime.parse(currentWeather['time'] as String)
: DateTime.now(),
);
}
Null safety techniques:
- ? operator checks for null before type casting
- ?? provides default values for missing fields
- Explicit null checks for critical nested objects
- Fallback values (like 0.0 or DateTime.now()) prevent crashes
5. Integrating Components
Create a main function to tie together the service and model:
// bin/main.dart
import 'package:weather_parser/weather_service.dart';
import 'package:weather_parser/weather_model.dart';
void main() async {
// Example coordinates (New York City)
const double latitude = 40.7128;
const double longitude = -74.0060;
// Create weather service instance
final weatherService = WeatherService();
try {
// Fetch raw data
final jsonData = await weatherService.fetchWeather(
latitude: latitude,
longitude: longitude,
);
// Convert to Weather object
final weather = Weather.fromJson(jsonData);
// Display formatted results
print('=== Current Weather ===');
print('Location: New York City');
print('Temperature: ${weather.temperature}°C');
print('Condition: ${weather.weatherCondition}');
print('Wind Speed: ${weather.windspeed} km/h');
print('Observation Time: ${weather.time.toLocal()}');
} catch (e) {
// Handle all errors gracefully
print('Error: $e');
}
}
Output example:
=== Current Weather ===
Location: New York City
Temperature: 18.5°C
Condition: Partly cloudy
Wind Speed: 12.3 km/h
Observation Time: 2023-10-15 14:30:00
III. Advanced Enhancements
1. User Input for Coordinates
Let users enter their own coordinates using dart:io:
import 'dart:io';
void main() async {
stdout.write('Enter latitude: ');
final latInput = stdin.readLineSync();
stdout.write('Enter longitude: ');
final lonInput = stdin.readLineSync();
final latitude = double.tryParse(latInput ?? '');
final longitude = double.tryParse(lonInput ?? '');
if (latitude == null || longitude == null) {
print('Invalid coordinates. Please enter numbers.');
return;
}
// Rest of the weather fetching logic...
}
2. Caching Recent Results
Add simple caching to reduce API calls:
class WeatherService {
Weather? _cachedWeather;
DateTime? _cacheTime;
Future<Weather> getCachedWeather({
required double latitude,
required double longitude,
}) async {
// Return cached data if less than 5 minutes old
if (_cachedWeather != null && _cacheTime != null) {
final cacheAge = DateTime.now().difference(_cacheTime!);
if (cacheAge.inMinutes < 5) {
return _cachedWeather!;
}
}
// Fetch new data and update cache
final jsonData = await fetchWeather(
latitude: latitude,
longitude: longitude,
);
_cachedWeather = Weather.fromJson(jsonData);
_cacheTime = DateTime.now();
return _cachedWeather!;
}
}
Top comments (1)
If you wanna beef up the error handling in your weather data parser, here’s the play: throw in some retry logic with exponential backoff so you don’t get smacked by rate limits too hard. When the server’s acting up, show the user a chill message and try again after a short nap. Make sure you're watching out for those pesky HTTP status codes - 400 means you probably messed up the request, 429 means you're asking too much, and anything 500+ means the server’s throwing a tantrum. Also, don’t let your app hang forever - slap some timeouts on your HTTP requests. And if you're doing this in Dart, async/await is your best bud for keeping the code clean and readable. Offload the heavy stuff to isolates, and keep those HTTP connections warm and persistent for better performance.