What You'll Build
A screen with a city text field and a search button. Submitting triggers a new Future that FutureBuilder turns into a spinner, an error message, or the weather details — with no manual state juggling.
Prerequisites
- A Flutter project with
http: ^1.2.0added topubspec.yaml - A free API key from OpenWeatherMap
- Read Networking with http and Futures & FutureBuilder first if these are new to you
Step by Step
1 Model the API response
// lib/weather.dart
class Weather {
final String city;
final double temp;
final double feelsLike;
final int humidity;
final String description;
Weather({
required this.city,
required this.temp,
required this.feelsLike,
required this.humidity,
required this.description,
});
factory Weather.fromJson(Map<String, dynamic> json) {
return Weather(
city: json['name'],
temp: (json['main']['temp'] as num).toDouble(),
feelsLike: (json['main']['feels_like'] as num).toDouble(),
humidity: json['main']['humidity'],
description: json['weather'][0]['description'],
);
}
}
2 Write the networking function
// lib/weather_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'weather.dart';
const _apiKey = 'YOUR_API_KEY';
Future<Weather> fetchWeather(String city) async {
final uri = Uri.parse(
'https://api.openweathermap.org/data/2.5/weather'
'?q=$city&appid=$_apiKey&units=metric',
);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Could not load weather for "$city"');
}
return Weather.fromJson(jsonDecode(response.body));
}
Throwing an Exception on a non-200 response lets FutureBuilder pick it up automatically through snapshot.hasError — no separate error-state variable needed.
3 Build the screen
The key idea: the Future lives in a field, created only when the user searches — never called directly inside build(), which would restart the request on every rebuild.
// lib/main.dart
import 'package:flutter/material.dart';
import 'weather_service.dart';
import 'weather.dart';
void main() => runApp(const WeatherApp());
class WeatherApp extends StatelessWidget {
const WeatherApp({super.key});
@override
Widget build(BuildContext context) =>
MaterialApp(home: const WeatherScreen(), debugShowCheckedModeBanner: false);
}
class WeatherScreen extends StatefulWidget {
const WeatherScreen({super.key});
@override
State<WeatherScreen> createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
final _controller = TextEditingController();
Future<Weather>? _weatherFuture;
void _search() {
final city = _controller.text.trim();
if (city.isEmpty) return;
setState(() {
_weatherFuture = fetchWeather(city);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Weather')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'City name'),
onSubmitted: (_) => _search(),
),
),
ElevatedButton(onPressed: _search, child: const Text('Search')),
],
),
const SizedBox(height: 24),
Expanded(
child: _weatherFuture == null
? const Text('Enter a city to see the weather.')
: FutureBuilder<Weather>(
future: _weatherFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('⚠️ ${snapshot.error}'));
}
final w = snapshot.data!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(w.city, style: Theme.of(context).textTheme.headlineMedium),
Text('${w.temp.toStringAsFixed(1)}°C — feels like ${w.feelsLike.toStringAsFixed(1)}°C'),
Text(w.description),
Text('Humidity: ${w.humidity}%'),
],
);
},
),
),
],
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
4 Run it
Run the app, type a city, tap Search. You should see a spinner briefly, then either the weather details or an error — try an invalid city to confirm the error branch renders correctly.
Final Working Code
Three files — lib/weather.dart, lib/weather_service.dart, lib/main.dart — exactly as shown above form the complete, working app.