Building Weather Forecasting Apps with Flutter and OpenWeatherMap API

Creating weather forecasting apps is a popular project for mobile developers due to the widespread utility of weather information. Flutter, with its cross-platform capabilities and rich UI toolkit, is an excellent choice for building such apps. Combined with the OpenWeatherMap API, developers can easily fetch real-time weather data and present it in an attractive and user-friendly interface.

Why Flutter for Weather Apps?

  • Cross-Platform: Build for iOS and Android with a single codebase.
  • Hot-Reload: Quickly test and iterate on UI changes.
  • Rich UI: Flutter’s extensive widget library allows for creating beautiful and responsive user interfaces.

Why OpenWeatherMap API?

  • Comprehensive Data: Access real-time weather data, forecasts, historical data, and more.
  • Free Tier: A generous free tier allows developers to start building without immediate cost.
  • Easy to Use: Simple API endpoints make it easy to fetch weather data in JSON format.

Step-by-Step Guide to Building a Weather App with Flutter and OpenWeatherMap API

Step 1: Setting Up the Flutter Project

First, ensure you have Flutter installed and set up on your development machine. Then, create a new Flutter project:

flutter create weather_app

Navigate to your project directory:

cd weather_app

Step 2: Adding Dependencies

Add the following dependencies to your pubspec.yaml file:

  • http: For making HTTP requests to the OpenWeatherMap API.
  • geolocator: For getting the user’s current location.
  • intl: For formatting dates and times.
  • flutter_svg: For displaying weather icons in SVG format (optional but recommended).
dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.5
  geolocator: ^9.0.2
  intl: ^0.17.0
  flutter_svg: ^1.1.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

Run flutter pub get to install the dependencies.

Step 3: Obtaining an OpenWeatherMap API Key

Go to OpenWeatherMap and create an account. Once you’ve logged in, navigate to the API keys section and generate a new API key. Keep this key safe as you’ll need it to make API requests.

Step 4: Setting Up Location Services

Add necessary permissions for location services in both AndroidManifest.xml (for Android) and Info.plist (for iOS).

AndroidManifest.xml

    
    
    ...
Info.plist

    NSLocationWhenInUseUsageDescription
    This app needs access to your location to provide accurate weather data.
    ...

Implement a function to get the current location:

import 'package:geolocator/geolocator.dart';

Future determinePosition() async {
  bool serviceEnabled;
  LocationPermission permission;

  serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    return Future.error('Location services are disabled.');
  }

  permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      return Future.error('Location permissions are denied');
    }
  }
  
  if (permission == LocationPermission.deniedForever) {
    return Future.error(
        'Location permissions are permanently denied, we cannot request permissions.');
  } 

  return await Geolocator.getCurrentPosition();
}

Step 5: Fetching Weather Data from OpenWeatherMap

Create a function to fetch weather data using the OpenWeatherMap API:

import 'dart:convert';
import 'package:http/http.dart' as http;

const apiKey = 'YOUR_OPENWEATHERMAP_API_KEY';
const weatherApiUrl = 'https://api.openweathermap.org/data/2.5/weather';

Future> fetchWeatherData(double latitude, double longitude) async {
  final url = '$weatherApiUrl?lat=$latitude&lon=$longitude&appid=$apiKey&units=metric';
  final response = await http.get(Uri.parse(url));

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to load weather data');
  }
}

Replace 'YOUR_OPENWEATHERMAP_API_KEY' with your actual API key.

Step 6: Designing the UI

Create a Flutter widget to display the weather data. Here’s a simple example:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_svg/flutter_svg.dart';

class WeatherScreen extends StatefulWidget {
  @override
  _WeatherScreenState createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State {
  String? cityName;
  double? temperature;
  String? description;
  String? iconCode;

  @override
  void initState() {
    super.initState();
    _loadWeatherData();
  }

  Future _loadWeatherData() async {
    try {
      final position = await determinePosition();
      final weatherData = await fetchWeatherData(position.latitude, position.longitude);

      setState(() {
        cityName = weatherData['name'];
        temperature = weatherData['main']['temp'];
        description = weatherData['weather'][0]['description'];
        iconCode = weatherData['weather'][0]['icon'];
      });
    } catch (e) {
      print('Error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Weather App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (cityName != null)
              Text(
                cityName!,
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
            if (temperature != null)
              Text(
                '${temperature!.toStringAsFixed(1)}°C',
                style: TextStyle(fontSize: 48),
              ),
            if (iconCode != null)
              SvgPicture.network(
                'https://openweathermap.org/img/wn/$iconCode@4x.png',
                width: 100,
                height: 100,
                placeholderBuilder: (BuildContext context) => CircularProgressIndicator(),
              ),
            if (description != null)
              Text(
                description!,
                style: TextStyle(fontSize: 18),
              ),
          ],
        ),
      ),
    );
  }
}

Modify your main.dart to use the WeatherScreen:

import 'package:flutter/material.dart';
import 'weather_screen.dart'; // Ensure the path is correct

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Weather App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WeatherScreen(),
    );
  }
}

Step 7: Running the App

Run your Flutter app on an emulator or physical device:

flutter run

Enhancements and Additional Features

  • Error Handling: Implement proper error handling for API requests and location services.
  • UI Improvements: Enhance the UI with better styling, animations, and responsive design.
  • Forecast Display: Fetch and display multi-day forecast data.
  • City Search: Allow users to search for weather data by city name.
  • Background Updates: Implement background updates to keep the weather data current.

Complete Example: Enhanced Weather App

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:flutter_svg/flutter_svg.dart';

// Replace with your API key
const apiKey = 'YOUR_OPENWEATHERMAP_API_KEY';
const weatherApiUrl = 'https://api.openweathermap.org/data/2.5/weather';
const forecastApiUrl = 'https://api.openweathermap.org/data/2.5/forecast';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Weather App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        fontFamily: 'Roboto',
      ),
      debugShowCheckedModeBanner: false,
      home: WeatherScreen(),
    );
  }
}

class WeatherScreen extends StatefulWidget {
  @override
  _WeatherScreenState createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State {
  String? cityName;
  double? temperature;
  String? description;
  String? iconCode;
  List? forecastList;
  bool isLoading = true; // Add loading state
  String? errorMessage;

  @override
  void initState() {
    super.initState();
    _loadWeatherData();
  }

  Future _loadWeatherData() async {
    setState(() {
      isLoading = true; // Start loading
      errorMessage = null;
    });

    try {
      final position = await determinePosition();
      final weatherData = await fetchWeatherData(position.latitude, position.longitude);
      final forecastData = await fetchForecastData(position.latitude, position.longitude);

      setState(() {
        cityName = weatherData['name'];
        temperature = weatherData['main']['temp'];
        description = weatherData['weather'][0]['description'];
        iconCode = weatherData['weather'][0]['icon'];
        forecastList = parseForecastData(forecastData);
      });
    } catch (e) {
      print('Error: $e');
      setState(() {
        errorMessage = 'Failed to load weather data: $e';
      });
    } finally {
      setState(() {
        isLoading = false; // Stop loading
      });
    }
  }

  Future> fetchWeatherData(double latitude, double longitude) async {
    final url = '$weatherApiUrl?lat=$latitude&lon=$longitude&appid=$apiKey&units=metric';
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load weather data');
    }
  }

  Future> fetchForecastData(double latitude, double longitude) async {
    final url = '$forecastApiUrl?lat=$latitude&lon=$longitude&appid=$apiKey&units=metric';
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load forecast data');
    }
  }

  List parseForecastData(Map forecastData) {
    List forecasts = [];
    final List list = forecastData['list'];
    
    for (var item in list) {
      forecasts.add(
        Forecast(
          dateTime: DateTime.parse(item['dt_txt']),
          temperature: item['main']['temp'],
          iconCode: item['weather'][0]['icon'],
        ),
      );
    }
    return forecasts;
  }

  Future determinePosition() async {
    bool serviceEnabled;
    LocationPermission permission;

    serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return Future.error('Location services are disabled.');
    }

    permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('Location permissions are denied');
      }
    }
    
    if (permission == LocationPermission.deniedForever) {
      return Future.error(
          'Location permissions are permanently denied, we cannot request permissions.');
    } 

    return await Geolocator.getCurrentPosition();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Weather App'),
        centerTitle: true,
        backgroundColor: Colors.blue[700],
      ),
      body: Center(
        child: isLoading
            ? CircularProgressIndicator()
            : errorMessage != null
                ? Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                      errorMessage!,
                      style: TextStyle(fontSize: 16, color: Colors.red),
                      textAlign: TextAlign.center,
                    ),
                )
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        cityName ?? 'Loading...',
                        style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue[900]),
                        textAlign: TextAlign.center,
                      ),
                      SizedBox(height: 10),
                      Text(
                        temperature != null ? '${temperature!.toStringAsFixed(1)}°C' : 'Loading...',
                        style: TextStyle(fontSize: 56, fontWeight: FontWeight.w500),
                      ),
                      SizedBox(height: 10),
                      if (iconCode != null)
                        SvgPicture.network(
                          'https://openweathermap.org/img/wn/$iconCode@4x.png',
                          width: 120,
                          height: 120,
                          placeholderBuilder: (BuildContext context) => CircularProgressIndicator(),
                        ),
                      Text(
                        description ?? 'Loading...',
                        style: TextStyle(fontSize: 20, color: Colors.grey[700]),
                      ),
                      SizedBox(height: 20),
                      Text(
                        'Forecast',
                        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue[700]),
                      ),
                      SizedBox(height: 10),
                      if (forecastList != null)
                        Container(
                          height: 150,
                          child: ListView.builder(
                            scrollDirection: Axis.horizontal,
                            itemCount: forecastList!.length,
                            itemBuilder: (context, index) {
                              return ForecastItem(forecast: forecastList![index]);
                            },
                          ),
                        ),
                    ],
                  ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadWeatherData,
        child: Icon(Icons.refresh),
        tooltip: 'Refresh Weather',
      ),
    );
  }
}

class Forecast {
  final DateTime dateTime;
  final double temperature;
  final String iconCode;

  Forecast({required this.dateTime, required this.temperature, required this.iconCode});
}

class ForecastItem extends StatelessWidget {
  final Forecast forecast;

  ForecastItem({required this.forecast});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8),
      child: Column(
        children: [
          Text(
            DateFormat('HH:mm').format(forecast.dateTime),
            style: TextStyle(fontSize: 16, color: Colors.blue[800]),
          ),
          SvgPicture.network(
            'https://openweathermap.org/img/wn/${forecast.iconCode}@2x.png',
            width: 60,
            height: 60,
            placeholderBuilder: (BuildContext context) => CircularProgressIndicator(),
          ),
          Text(
            '${forecast.temperature.toStringAsFixed(1)}°C',
            style: TextStyle(fontSize: 18),
          ),
        ],
      ),
    );
  }
}

This example incorporates:

  • Enhanced UI with forecast display.
  • Loading state indicator.
  • Error handling messages.

Conclusion

Building weather forecasting apps with Flutter and OpenWeatherMap API is an excellent way to explore cross-platform mobile development. By combining Flutter’s UI capabilities with the comprehensive data from OpenWeatherMap, developers can create functional and visually appealing weather applications. Whether for personal projects or commercial applications, this guide provides a solid foundation for getting started.