Creating a Weather Forecast Widget with API in Flutter
Building dynamic and interactive applications often involves integrating external data sources. A common and practical example is a weather forecast widget, which fetches real-time weather data from an API and displays it within a Flutter application. This article will guide you through the process of creating such a widget, covering API integration, data modeling, and UI implementation.
Prerequisites
- Basic understanding of Flutter and Dart.
- Flutter SDK installed and configured.
- A code editor (VS Code, Android Studio).
1. Choosing a Weather API and Obtaining an API Key
Several weather APIs are available, both free and paid. For this tutorial, we will use OpenWeatherMap, which offers a free tier suitable for development. Follow these steps:
- Go to OpenWeatherMap API and sign up for a free account.
- Once logged in, navigate to the "API keys" section to find your unique API key. Keep this key secure, as it's essential for making API requests.
OpenWeatherMap provides various endpoints, including current weather data and 5-day/3-hour forecasts. We'll focus on the latter for a comprehensive forecast widget.
2. Setting Up Your Flutter Project
First, create a new Flutter project:
flutter create weather_app
cd weather_app
Next, add the necessary dependencies to your pubspec.yaml file. We'll need http for making network requests and intl for date formatting.
dependencies:
flutter:
sdk: flutter
http: ^1.1.0 # Or the latest stable version
intl: ^0.18.1 # Or the latest stable version
After modifying pubspec.yaml, run flutter pub get to fetch the new packages.
3. Data Modeling
The weather API will return data in JSON format. To work with this data effectively in Dart, it's best to create a data model. This involves defining classes that mirror the structure of the JSON response. For OpenWeatherMap's 5-day/3-hour forecast, we'll simplify it to capture key information like date, temperature, and weather description.
Create a file named lib/models/weather_data.dart:
// lib/models/weather_data.dart
class Weather {
final String main;
final String description;
final String icon;
Weather({required this.main, required this.description, required this.icon});
factory Weather.fromJson(Map<String, dynamic> json) {
return Weather(
main: json['main'],
description: json['description'],
icon: json['icon'],
);
}
}
class MainData {
final double temp;
final double tempMin;
final double tempMax;
final int humidity;
MainData({
required this.temp,
required this.tempMin,
required this.tempMax,
required this.humidity,
});
factory MainData.fromJson(Map<String, dynamic> json) {
return MainData(
temp: (json['temp'] as num).toDouble(),
tempMin: (json['temp_min'] as num).toDouble(),
tempMax: (json['temp_max'] as num).toDouble(),
humidity: json['humidity'],
);
}
}
class Forecast {
final DateTime date;
final MainData main;
final List<Weather> weather;
Forecast({
required this.date,
required this.main,
required this.weather,
});
factory Forecast.fromJson(Map<String, dynamic> json) {
var weatherList = json['weather'] as List;
List<Weather> weatherItems =
weatherList.map((i) => Weather.fromJson(i)).toList();
return Forecast(
date: DateTime.fromMillisecondsSinceEpoch(json['dt'] * 1000),
main: MainData.fromJson(json['main']),
weather: weatherItems,
);
}
}
class WeatherForecast {
final List<Forecast> list;
final String cityName;
WeatherForecast({required this.list, required this.cityName});
factory WeatherForecast.fromJson(Map<String, dynamic> json) {
var list = json['list'] as List;
List<Forecast> forecastList =
list.map((i) => Forecast.fromJson(i)).toList();
return WeatherForecast(
list: forecastList,
cityName: json['city']['name'],
);
}
}
4. Creating an API Service
Next, create a service class that will handle making the HTTP requests to OpenWeatherMap. This class will parse the JSON response into our Dart data model.
Create a file named lib/services/weather_service.dart:
// lib/services/weather_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/weather_data.dart';
class WeatherService {
final String apiKey;
final String baseUrl = 'https://api.openweathermap.org/data/2.5';
WeatherService({required this.apiKey});
Future<WeatherForecast> fetchWeatherForecast(String city) async {
final response = await http.get(Uri.parse(
'$baseUrl/forecast?q=$city&appid=$apiKey&units=metric')); // units=metric for Celsius
if (response.statusCode == 200) {
return WeatherForecast.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load weather forecast: ${response.statusCode}');
}
}
String getWeatherIconUrl(String iconCode) {
return 'http://openweathermap.org/img/wn/[email protected]';
}
}
5. Building the Weather Widget UI
Now, let's integrate everything into our Flutter UI. We'll use a FutureBuilder to asynchronously fetch the weather data and display a loading indicator while the data is being fetched. We'll show the current weather and a list of forecast items.
Modify your lib/main.dart file:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'models/weather_data.dart';
import 'services/weather_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Weather App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const WeatherHomePage(),
);
}
}
class WeatherHomePage extends StatefulWidget {
const WeatherHomePage({super.key});
@override
State<WeatherHomePage> createState() => _WeatherHomePageState();
}
class _WeatherHomePageState extends State<WeatherHomePage> {
late Future<WeatherForecast> _weatherForecast;
final String _cityName = 'London'; // Example city
final WeatherService _weatherService = WeatherService(apiKey: 'YOUR_API_KEY'); // <-- Replace with your actual API Key
@override
void initState() {
super.initState();
_weatherForecast = _weatherService.fetchWeatherForecast(_cityName);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Weather in $_cityName'),
),
body: FutureBuilder<WeatherForecast>(
future: _weatherForecast,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final weatherForecast = snapshot.data!;
final currentForecast = weatherForecast.list.first; // Get current weather from the first entry
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Current Weather Section
_buildCurrentWeather(currentForecast, weatherForecast.cityName),
const SizedBox(height: 20),
// Forecast List
_buildForecastList(weatherForecast.list),
],
),
);
} else {
return const Center(child: Text('No weather data available.'));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_weatherForecast = _weatherService.fetchWeatherForecast(_cityName);
});
},
child: const Icon(Icons.refresh),
),
);
}
Widget _buildCurrentWeather(Forecast current, String cityName) {
return Column(
children: [
Text(
cityName,
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
DateFormat('EEEE, MMM d, yyyy').format(current.date),
style: const TextStyle(fontSize: 18, color: Colors.grey),
),
const SizedBox(height: 16),
Image.network(
_weatherService.getWeatherIconUrl(current.weather.first.icon),
width: 100,
height: 100,
),
Text(
'${current.main.temp.round()}°C',
style: const TextStyle(fontSize: 64, fontWeight: FontWeight.w300),
),
Text(
current.weather.first.description.toUpperCase(),
style: const TextStyle(fontSize: 20, fontStyle: FontStyle.italic),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Min: ${current.main.tempMin.round()}°C', style: const TextStyle(fontSize: 16)),
const SizedBox(width: 10),
Text('Max: ${current.main.tempMax.round()}°C', style: const TextStyle(fontSize: 16)),
],
),
Text('Humidity: ${current.main.humidity}%', style: const TextStyle(fontSize: 16)),
],
);
}
Widget _buildForecastList(List<Forecast> forecasts) {
// Group forecasts by day
Map<String, List<Forecast>> dailyForecasts = {};
for (var forecast in forecasts) {
String dateKey = DateFormat('yyyy-MM-dd').format(forecast.date);
if (!dailyForecasts.containsKey(dateKey)) {
dailyForecasts[dateKey] = [];
}
dailyForecasts[dateKey]!.add(forecast);
}
// Display forecasts for the next few days (skipping the current day's multiple entries for simplicity)
List<Widget> dailyWidgets = [];
int daysToShow = 5; // Display up to 5 days
int dayCount = 0;
dailyForecasts.forEach((dateKey, dayEntries) {
if (dayCount < daysToShow) {
// Use the first entry of the day to represent the day's forecast
// Or aggregate min/max temp for the day
final representativeForecast = dayEntries.first;
// Skip the current day if we only want future forecasts
if (DateFormat('yyyy-MM-dd').format(DateTime.now()) == dateKey && dayCount == 0) {
// This is the current day, we already displayed its main info.
// We can choose to skip it here or include hourly forecast.
// For a simple daily summary, we will skip it and start from tomorrow.
// If you want hourly breakdown for current day, you'd iterate through dayEntries here.
dayCount++; // Count current day but don't add a summary card if only showing future days.
return;
}
dailyWidgets.add(
Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('EEE, MMM d').format(representativeForecast.date),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
representativeForecast.weather.first.description,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
Expanded(
flex: 1,
child: Image.network(
_weatherService.getWeatherIconUrl(representativeForecast.weather.first.icon),
width: 50,
height: 50,
),
),
Expanded(
flex: 1,
child: Text(
'${representativeForecast.main.temp.round()}°C',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
textAlign: TextAlign.right,
),
),
],
),
),
),
);
dayCount++;
}
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Next 5 Days Forecast:',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
...dailyWidgets, // Spread operator to add all widgets from the list
],
);
}
}
Important: Remember to replace 'YOUR_API_KEY' with your actual OpenWeatherMap API key in lib/main.dart.
Running the Application
To run your Flutter weather app, connect a device or start an emulator and execute:
flutter run
Conclusion
You have successfully built a weather forecast widget in Flutter by integrating with a third-party API. This process involved setting up dependencies, defining data models, creating a service to fetch and parse data, and finally, designing the UI to display the information using a FutureBuilder for asynchronous data handling. This foundational knowledge can be extended to integrate with other APIs and build more complex dynamic features in your Flutter applications.