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.

📱 Flutter🌦️ API Open-Meteo🧩 7 fichiers⏱️ 20 min de lecture

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.

Application météo Flutter : météo actuelle à Bruxelles
La météo actuelle
Application météo Flutter : prévisions sur 7 jours
Les prévisions sur 7 jours

Ce que tu vas apprendre avec cette application météo Flutter

🌐 Appels HTTPUtiliser le package http et gérer les erreurs réseau.
🧾 JSON → objets DartTransformer une réponse d’API en modèles typés.
🔄 États de l’écranAfficher chargement, erreur ou résultat avec setState.
🎨 Material 3Thème clair et sombre à partir d’une seule couleur.

Prérequis pour créer l’application météo Flutter

  • Flutter installé et fonctionnel pour compiler l’application météo Flutter (flutter doctor sans 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 :

APIRôleAdresse
GéocodageTrouver la latitude et la longitude d’une villegeocoding-api.open-meteo.com/v1/search
PrévisionsMétéo actuelle et prévisions journalièresapi.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=auto
URL de test
ℹ️
Conditions d’utilisation :

l’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 http
terminal

Pour 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 principal
structure 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: true
pubspec.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.dart
💡
Pourquoi des modèles ?

Sans 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.dart
  • Uri.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: auto renvoie 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
⏳ _chargementAffiche un indicateur de progression pendant l’appel à l’API.
⚠️ _erreurAffiche le message et un bouton « Réessayer ».
✅ _meteoAffiche la carte actuelle et la liste des 7 jours.
🔄 RefreshIndicatorTire la liste vers le bas pour actualiser.

É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 chrome
terminal

Au 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.

Application météo Flutter : message d’erreur quand la ville est introuvable
Ville introuvable : un message clair
Application météo Flutter en mode sombre, météo à Liège
Le mode sombre, automatique
⚠️
Android en version release :

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.xml

Pour 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èmeSolution
Target of URI doesn’t exist: package:httpLance flutter pub add http puis flutter pub get.
« Connexion impossible » sur Android en releaseAjoute la permission INTERNET dans le manifeste.
Target of URI doesn’t exist: screens/home_screen.dartVérifie le nom et l’emplacement des fichiers dans lib/.
Températures toujours entièresNormal : on arrondit avec round(). Utilise toStringAsFixed(1) pour une décimale.

Améliorer ton application météo Flutter : 5 exercices

  1. Mémoriser la dernière ville de ton application météo Flutter avec le package shared_preferences.
  2. Géolocalisation : afficher dans ton application météo Flutter la météo de la position actuelle avec le package geolocator.
  3. Prévisions horaires : ajouter le paramètre hourly=temperature_2m et une liste horizontale.
  4. Plusieurs résultats : passer count à 5 et proposer une liste de villes.
  5. Tests : pour fiabiliser ton application météo Flutter, injecter un faux http.Client dans MeteoService pour 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

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *