DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 24: Core Features in Action — Weather API Data Parsing

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
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

dart pub get
Enter fullscreen mode Exit fullscreen mode

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');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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(),
  );
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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!;
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
onlineproxy profile image
OnlineProxy

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.