Dans ce tutoriel, tu vas créer une application météo complète en Flutter : recherche d’une ville, météo actuelle, prévisions sur 7 jours, gestion des erreurs et mode sombre automatique. Les données viennent de l’API gratuite Open-Meteo, qui ne demande aucune clé. Tout le code source est dans l’article : tu le recopies fichier par fichier, et tu comprends chaque ligne au passage.
Cette application météo Flutter est un excellent projet pour apprendre : il couvre les appels HTTP, la lecture de JSON, les modèles de données, la gestion des états (chargement, erreur, résultat) et la mise en page Material 3. Avant de coder ton application météo Flutter, si tu hésites encore entre les frameworks, lis notre comparatif React Native ou Flutter.
Les captures ci-dessous montrent l’application météo Flutter obtenue avec exactement le code de cet article.


Ce que tu vas apprendre avec cette application météo Flutter
Prérequis pour créer l’application météo Flutter
- Flutter installé et fonctionnel pour compiler l’application météo Flutter (
flutter doctorsans erreur bloquante). (source : Flutter : installation) - Un éditeur : VS Code avec l’extension Flutter, ou Android Studio.
- Un émulateur Android, un iPhone/simulateur, ou simplement Chrome pour tester.
- Les bases de Dart : classes,
async/await.
L’API Open-Meteo de l’application météo Flutter : gratuite et sans clé
Ton application météo Flutter utilise deux API d’Open-Meteo :
| API | Rôle | Adresse |
|---|---|---|
| Géocodage | Trouver la latitude et la longitude d’une ville | geocoding-api.open-meteo.com/v1/search |
| Prévisions | Météo actuelle et prévisions journalières | api.open-meteo.com/v1/forecast |
Sources : API de géocodage, documentation Open-Meteo.
Tu peux tester une requête de l’application météo Flutter directement dans ton navigateur :
https://api.open-meteo.com/v1/forecast?latitude=50.85&longitude=4.35¤t=temperature_2m,weather_code&daily=temperature_2m_max,temperature_2m_min&timezone=autoURL de testl’API est gratuite pour un usage non commercial, dans la limite d’un nombre raisonnable de requêtes, et les données doivent être attribuées à Open-Meteo. Pour une app commerciale, consulte leurs offres payantes. (source : Open-Meteo : conditions d’utilisation)
Étape 1 : créer le projet de l’application météo Flutter
flutter create meteo_flutter
cd meteo_flutter
flutter pub add httpterminalPour ton application météo Flutter, la commande flutter pub add http ajoute le package officiel http, qui sert à appeler l’API. (source : package http (pub.dev))
Étape 2 : la structure du projet
Pour organiser ton application météo Flutter, crée les dossiers et fichiers suivants dans lib/. Séparer le code par rôle rend le projet plus facile à lire et à faire évoluer.
lib/
├── main.dart # Point d’entrée et thème
├── models/
│ └── meteo.dart # Ville, MeteoActuelle, PrevisionJour, Meteo
├── services/
│ └── meteo_service.dart # Appels à l’API Open-Meteo
├── utils/
│ └── weather_code.dart # Code météo → libellé + icône
├── widgets/
│ └── forecast_tile.dart # Une ligne de prévision
└── screens/
└── home_screen.dart # L’écran principalstructure du projetÉtape 3 : le fichier pubspec.yaml
Après flutter pub add http, la partie utile du pubspec.yaml de ton application météo Flutter doit ressembler à ceci (la version de http peut être plus récente chez toi) :
name: meteo_flutter
description: Application météo Flutter avec l'API gratuite Open-Meteo.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
http: ^1.2.2
flutter:
uses-material-design: truepubspec.yamlÉtape 4 : les modèles de données (models/meteo.dart)
Dans l’application météo Flutter, les modèles transforment le JSON de l’API en objets Dart typés. Chaque classe a un constructeur fromJson qui lit les champs dont on a besoin. On convertit les nombres avec (… as num).toDouble(), car l’API peut renvoyer 7 ou 7.0.
/// Une ville trouvée par l'API de géocodage.
class Ville {
final String nom;
final String pays;
final double latitude;
final double longitude;
const Ville({
required this.nom,
required this.pays,
required this.latitude,
required this.longitude,
});
factory Ville.fromJson(Map<String, dynamic> json) {
return Ville(
nom: json['name'] as String,
pays: (json['country'] ?? '') as String,
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
);
}
}
/// La météo actuelle.
class MeteoActuelle {
final double temperature;
final double ressentie;
final int humidite;
final double vent;
final int code;
const MeteoActuelle({
required this.temperature,
required this.ressentie,
required this.humidite,
required this.vent,
required this.code,
});
factory MeteoActuelle.fromJson(Map<String, dynamic> json) {
return MeteoActuelle(
temperature: (json['temperature_2m'] as num).toDouble(),
ressentie: (json['apparent_temperature'] as num).toDouble(),
humidite: (json['relative_humidity_2m'] as num).toInt(),
vent: (json['wind_speed_10m'] as num).toDouble(),
code: (json['weather_code'] as num).toInt(),
);
}
}
/// La prévision pour un jour.
class PrevisionJour {
final DateTime date;
final double min;
final double max;
final int code;
const PrevisionJour({
required this.date,
required this.min,
required this.max,
required this.code,
});
}
/// Tout ce que l'écran affiche pour une ville.
class Meteo {
final Ville ville;
final MeteoActuelle actuelle;
final List<PrevisionJour> jours;
const Meteo({
required this.ville,
required this.actuelle,
required this.jours,
});
factory Meteo.fromJson(Ville ville, Map<String, dynamic> json) {
final daily = json['daily'] as Map<String, dynamic>;
final dates = daily['time'] as List;
final jours = <PrevisionJour>[];
for (var i = 0; i < dates.length; i++) {
jours.add(PrevisionJour(
date: DateTime.parse(dates[i] as String),
min: (daily['temperature_2m_min'][i] as num).toDouble(),
max: (daily['temperature_2m_max'][i] as num).toDouble(),
code: (daily['weather_code'][i] as num).toInt(),
));
}
return Meteo(
ville: ville,
actuelle: MeteoActuelle.fromJson(json['current'] as Map<String, dynamic>),
jours: jours,
);
}
}lib/models/meteo.dartSans eux, tu manipulerais partout des Map<String, dynamic> et une faute de frappe dans une clé ne serait détectée qu’à l’exécution. Avec des classes, l’éditeur t’aide et les erreurs sont repérées tout de suite.
Étape 5 : le service API (services/meteo_service.dart)
Le service de l’application météo Flutter fait deux appels : il cherche d’abord les coordonnées de la ville, puis demande la météo pour ces coordonnées. Toutes les erreurs (ville inconnue, serveur, pas de réseau) sont transformées en MeteoException avec un message compréhensible.
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/meteo.dart';
/// Erreur lisible par l'utilisateur.
class MeteoException implements Exception {
final String message;
const MeteoException(this.message);
@override
String toString() => message;
}
/// Accès aux API gratuites d'Open-Meteo (aucune clé nécessaire).
class MeteoService {
final http.Client _client;
MeteoService({http.Client? client}) : _client = client ?? http.Client();
/// 1. Trouve les coordonnées d'une ville.
Future<Ville> chercherVille(String nom) async {
final uri = Uri.https('geocoding-api.open-meteo.com', '/v1/search', {
'name': nom,
'count': '1',
'language': 'fr',
'format': 'json',
});
final data = await _get(uri);
final resultats = data['results'] as List?;
if (resultats == null || resultats.isEmpty) {
throw MeteoException('Aucune ville trouvée pour « $nom ».');
}
return Ville.fromJson(resultats.first as Map<String, dynamic>);
}
/// 2. Récupère la météo actuelle et les prévisions sur 7 jours.
Future<Meteo> meteoPour(Ville ville) async {
final uri = Uri.https('api.open-meteo.com', '/v1/forecast', {
'latitude': ville.latitude.toString(),
'longitude': ville.longitude.toString(),
'current': 'temperature_2m,relative_humidity_2m,apparent_temperature,'
'weather_code,wind_speed_10m',
'daily': 'weather_code,temperature_2m_max,temperature_2m_min',
'timezone': 'auto',
});
final data = await _get(uri);
return Meteo.fromJson(ville, data);
}
/// Raccourci : ville + météo en un seul appel.
Future<Meteo> meteoParNom(String nom) async {
final ville = await chercherVille(nom);
return meteoPour(ville);
}
Future<Map<String, dynamic>> _get(Uri uri) async {
try {
final reponse =
await _client.get(uri).timeout(const Duration(seconds: 10));
if (reponse.statusCode != 200) {
throw MeteoException('Erreur du serveur (${reponse.statusCode}).');
}
return jsonDecode(reponse.body) as Map<String, dynamic>;
} on MeteoException {
rethrow;
} catch (_) {
throw const MeteoException(
'Connexion impossible. Vérifie ta connexion Internet.');
}
}
}lib/services/meteo_service.dartUri.https(...)construit l’URL et encode correctement les paramètres (accents, espaces)..timeout(...)évite d’attendre indéfiniment si le réseau ne répond pas.timezone: autorenvoie les dates dans le fuseau horaire de la ville.
Étape 6 : traduire les codes météo (utils/weather_code.dart)
Open-Meteo renvoie l’état du ciel sous forme de code numérique (norme WMO) : 0 pour un ciel dégagé, 61 à 67 pour la pluie, 95 et plus pour l’orage, etc. Ce fichier de l’application météo Flutter les traduit en texte et en icône Material. (source : Open-Meteo : documentation)
import 'package:flutter/material.dart';
/// Traduit un code météo WMO (utilisé par Open-Meteo) en libellé et icône.
class CodeMeteo {
final String libelle;
final IconData icone;
const CodeMeteo(this.libelle, this.icone);
static CodeMeteo depuis(int code) {
if (code == 0) return const CodeMeteo('Ciel dégagé', Icons.wb_sunny);
if (code <= 2) return const CodeMeteo('Peu nuageux', Icons.wb_cloudy);
if (code == 3) return const CodeMeteo('Couvert', Icons.cloud);
if (code == 45 || code == 48) {
return const CodeMeteo('Brouillard', Icons.foggy);
}
if (code >= 51 && code <= 57) {
return const CodeMeteo('Bruine', Icons.grain);
}
if ((code >= 61 && code <= 67) || (code >= 80 && code <= 82)) {
return const CodeMeteo('Pluie', Icons.water_drop);
}
if ((code >= 71 && code <= 77) || code == 85 || code == 86) {
return const CodeMeteo('Neige', Icons.ac_unit);
}
if (code >= 95) return const CodeMeteo('Orage', Icons.thunderstorm);
return const CodeMeteo('Inconnu', Icons.help_outline);
}
}
/// Nom court du jour en français, sans dépendance externe.
String jourCourt(DateTime date) {
const jours = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
final aujourdhui = DateTime.now();
if (date.year == aujourdhui.year &&
date.month == aujourdhui.month &&
date.day == aujourdhui.day) {
return "Auj.";
}
return jours[date.weekday - 1];
}lib/utils/weather_code.dartÉtape 7 : une ligne de prévision (widgets/forecast_tile.dart)
Un petit widget réutilisable de l’application météo Flutter pour afficher un jour : icône, nom du jour, description et températures minimale et maximale.
import 'package:flutter/material.dart';
import '../models/meteo.dart';
import '../utils/weather_code.dart';
/// Une ligne de la prévision sur 7 jours.
class ForecastTile extends StatelessWidget {
final PrevisionJour jour;
const ForecastTile({super.key, required this.jour});
@override
Widget build(BuildContext context) {
final code = CodeMeteo.depuis(jour.code);
final couleurs = Theme.of(context).colorScheme;
return ListTile(
leading: Icon(code.icone, color: couleurs.primary),
title: Text(jourCourt(jour.date)),
subtitle: Text(code.libelle),
trailing: Text(
'${jour.min.round()}° / ${jour.max.round()}°',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
);
}
}lib/widgets/forecast_tile.dartÉtape 8 : l’écran principal (screens/home_screen.dart)
C’est le cœur de l’application météo Flutter. L’écran garde trois informations dans son état : la météo affichée, un éventuel message d’erreur et un indicateur de chargement. La méthode _contenu() choisit quoi afficher selon cet état.
import 'package:flutter/material.dart';
import '../models/meteo.dart';
import '../services/meteo_service.dart';
import '../utils/weather_code.dart';
import '../widgets/forecast_tile.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _service = MeteoService();
final _champ = TextEditingController(text: 'Bruxelles');
Meteo? _meteo;
String? _erreur;
bool _chargement = false;
@override
void initState() {
super.initState();
_rechercher();
}
@override
void dispose() {
_champ.dispose();
super.dispose();
}
Future<void> _rechercher() async {
final nom = _champ.text.trim();
if (nom.isEmpty) return;
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_chargement = true;
_erreur = null;
});
try {
final meteo = await _service.meteoParNom(nom);
setState(() => _meteo = meteo);
} on MeteoException catch (e) {
setState(() => _erreur = e.message);
} finally {
setState(() => _chargement = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Météo Flutter'), centerTitle: true),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: TextField(
controller: _champ,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _rechercher(),
decoration: InputDecoration(
hintText: 'Rechercher une ville',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: _rechercher,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
Expanded(child: _contenu()),
],
),
),
);
}
Widget _contenu() {
if (_chargement) {
return const Center(child: CircularProgressIndicator());
}
if (_erreur != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48),
const SizedBox(height: 12),
Text(_erreur!, textAlign: TextAlign.center),
const SizedBox(height: 12),
FilledButton(
onPressed: _rechercher,
child: const Text('Réessayer'),
),
],
),
),
);
}
final meteo = _meteo;
if (meteo == null) return const SizedBox.shrink();
return RefreshIndicator(
onRefresh: _rechercher,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_CarteActuelle(meteo: meteo),
const SizedBox(height: 16),
Text('Prévisions sur 7 jours',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Card(
child: Column(
children: [
for (final jour in meteo.jours) ForecastTile(jour: jour),
],
),
),
],
),
);
}
}
/// La grande carte avec la météo actuelle.
class _CarteActuelle extends StatelessWidget {
final Meteo meteo;
const _CarteActuelle({required this.meteo});
@override
Widget build(BuildContext context) {
final a = meteo.actuelle;
final code = CodeMeteo.depuis(a.code);
final couleurs = Theme.of(context).colorScheme;
return Card(
color: couleurs.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
'${meteo.ville.nom}, ${meteo.ville.pays}',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Icon(code.icone, size: 72, color: couleurs.primary),
Text(
'${a.temperature.round()}°C',
style: Theme.of(context)
.textTheme
.displayMedium
?.copyWith(fontWeight: FontWeight.bold),
),
Text(code.libelle, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_Info(Icons.thermostat, 'Ressenti', '${a.ressentie.round()}°'),
_Info(Icons.water_drop, 'Humidité', '${a.humidite} %'),
_Info(Icons.air, 'Vent', '${a.vent.round()} km/h'),
],
),
],
),
),
);
}
}
class _Info extends StatelessWidget {
final IconData icone;
final String titre;
final String valeur;
const _Info(this.icone, this.titre, this.valeur);
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(icone, size: 20),
const SizedBox(height: 4),
Text(valeur, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(titre, style: Theme.of(context).textTheme.bodySmall),
],
);
}
}lib/screens/home_screen.dartÉtape 9 : le point d’entrée (main.dart)
Remplace tout le contenu de lib/main.dart, le point d’entrée de l’application météo Flutter. Le thème Material 3 est généré à partir d’une seule couleur avec colorSchemeSeed, et ThemeMode.system suit automatiquement le mode clair ou sombre du téléphone.
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MeteoApp());
}
class MeteoApp extends StatelessWidget {
const MeteoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Météo Flutter',
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF2563EB),
brightness: Brightness.light,
useMaterial3: true,
),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF2563EB),
brightness: Brightness.dark,
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}lib/main.dartÉtape 10 : lancer l’application météo Flutter
flutter pub get
flutter run
# Ou dans le navigateur
flutter run -d chrometerminalAu démarrage, l’application météo Flutter affiche la météo de Bruxelles. Dans ton application météo Flutter, tape une autre ville et valide : la météo se met à jour.


en mode debug, l’accès à Internet fonctionne directement. Pour une version release, ajoute la permission Internet dans android/app/src/main/AndroidManifest.xml, sinon les appels à l’API échoueront. (source : Flutter : networking)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application ...>android/app/src/main/AndroidManifest.xmlPour publier ton app, suis ensuite notre guide Publier sur le Google Play Store.
Les erreurs fréquentes dans une application météo Flutter
| Problème | Solution |
|---|---|
Target of URI doesn’t exist: package:http | Lance flutter pub add http puis flutter pub get. |
| « Connexion impossible » sur Android en release | Ajoute la permission INTERNET dans le manifeste. |
Target of URI doesn’t exist: screens/home_screen.dart | Vérifie le nom et l’emplacement des fichiers dans lib/. |
| Températures toujours entières | Normal : on arrondit avec round(). Utilise toStringAsFixed(1) pour une décimale. |
Améliorer ton application météo Flutter : 5 exercices
- Mémoriser la dernière ville de ton application météo Flutter avec le package
shared_preferences. - Géolocalisation : afficher dans ton application météo Flutter la météo de la position actuelle avec le package
geolocator. - Prévisions horaires : ajouter le paramètre
hourly=temperature_2met une liste horizontale. - Plusieurs résultats : passer
countà 5 et proposer une liste de villes. - Tests : pour fiabiliser ton application météo Flutter, injecter un faux
http.ClientdansMeteoServicepour tester le service sans réseau.
Besoin d’autres API pour tes projets ? Consulte notre sélection d’API gratuites pour développeur, ou découvre notre autre projet Flutter : l’app de tri des déchets.
FAQ
L’API Open-Meteo est-elle vraiment gratuite ?
Oui, pour un usage non commercial, sans clé ni inscription, avec une attribution à Open-Meteo.
Cette application fonctionne-t-elle sur iPhone ?
Oui. Le code de cette application météo Flutter tourne sur Android, iOS et le Web.
Pourquoi deux appels à l’API ?
Dans cette application météo Flutter, l’API météo attend des coordonnées. On utilise d’abord l’API de géocodage pour convertir le nom de la ville en latitude et longitude.
Faut-il télécharger le code source ?
Non. Tout le code de l’application météo Flutter est dans l’article : recopie chaque fichier dans la structure indiquée.
Application météo Flutter : le résumé à retenir
- 7 fichiers pour l’application météo Flutter : main, modèles, service, utilitaires, widget, écran, pubspec.
- Open-Meteo : géocodage puis prévisions, sans clé.
- Trois états à gérer dans l’application météo Flutter : chargement, erreur, résultat.
- Permission INTERNET pour publier l’application météo Flutter sur Android en release.
Conclusion
Tu as maintenant une application météo Flutter complète et propre, organisée comme un vrai projet. Le meilleur moyen de progresser avec ton application météo Flutter : reprends-la et ajoute une des fonctionnalités de la liste d’exercices.
Sources
- Open-Meteo : Weather Forecast API
- Open-Meteo : Geocoding API
- Open-Meteo : Terms
- Flutter : Networking
- pub.dev : package http
- Flutter : installation

