Tutoriels IoT

Client MQTT Flutter : créer une application IoT multiplateforme

Un tutoriel Flutter MQTT pratique avec le package mqtt_client pour connecter des builds Android, iOS, desktop et web à un broker cloud via TLS ou WebSocket sécurisé, lire des données de capteurs en direct et piloter un appareil.

Application Flutter MQTT sur un téléphone et dans un navigateur affichant des valeurs de capteurs en direct et un interrupteur d'appareil connectés à un broker cloud
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Flutter MQTT permet à une seule base de code Dart de communiquer avec vos appareils depuis Android, iOS, le desktop et le web. Dans ce tutoriel, vous allez connecter une application Flutter à un broker cloud avec le package mqtt_client, afficher des relevés de capteurs en direct et envoyer des commandes marche/arrêt à un appareil, en utilisant TLS sur les plateformes natives et WebSocket sécurisé dans le navigateur.

Ce que vous allez créer

L'application comporte un seul écran : une température en direct qui se met à jour chaque fois qu'un appareil publie, une puce d'état de connexion et un interrupteur qui publie une commande pour allumer ou éteindre un relais. En coulisses, elle utilise deux topics :

Topic Sens Exemple de payload
connectmq/demo/<your-id>/sensor/temperature appareil vers application {"value": 22.4}
connectmq/demo/<your-id>/relay/set application vers appareil ON ou OFF

Pour les tests, nous utilisons le serveur MQTT public gratuit broker.connectmq.com. Il accepte les connexions anonymes avec n'importe quel client ID non vide, mais tout ce qui s'y trouve est lisible par n'importe qui : utilisez donc un préfixe de topic aléatoire et n'envoyez jamais de données réelles. Lors du passage en production, vous remplacez simplement par les identifiants de votre propre broker ConnectMQ ; la structure du code ne change pas.

Choisir un package MQTT pour Flutter

L'option la plus utilisée est mqtt_client sur pub.dev. Il fournit deux classes de client :

  • MqttServerClient pour Android, iOS, macOS, Windows et Linux. Il gère TCP en clair, TLS ou WebSocket.
  • MqttBrowserClient pour Flutter web, qui ne peut utiliser que WebSocket, car les navigateurs n'autorisent pas les sockets TCP bruts.

mqtt_client implémente MQTT 3.1.1. Si vous avez besoin de fonctionnalités MQTT 5 comme l'expiration des messages ou les user properties, le même auteur maintient mqtt5_client, dont l'API est très proche. Pour une application de tableau de bord et de commandes comme celle-ci, la 3.1.1 suffit amplement. Si vous comparez des bibliothèques entre langages, le panorama des bibliothèques client MQTT présente les compromis.

Ajoutez la dépendance :

flutter pub add mqtt_client

Ports et transports

Le broker expose quatre listeners, et vous en choisissez un par plateforme :

Plateforme Classe Transport Port
Android, iOS, desktop MqttServerClient TLS 8883
Android, iOS, desktop (derrière des pare-feu stricts) MqttServerClient avec useWebSocket WSS 8084, chemin /mqtt
Flutter web MqttBrowserClient WSS 8084, chemin /mqtt
Débogage local uniquement MqttServerClient TCP 1883

Le port 1883 n'est pas chiffré : réservez-le aux expériences rapides. Tout ce qui quitte votre portable doit utiliser 8883 ou 8084. Pour le contexte sur les transports côté navigateur, lisez MQTT over WebSocket.

Créer le service de connexion Flutter MQTT

Gardez MQTT hors de vos widgets. Une petite classe de service détient le client, expose un flux de températures et un état de connexion, et propose une méthode setRelay. Flutter web ne peut pas importer dart:io : nous créons donc le client via un import conditionnel.

lib/mqtt/client_factory.dart :

export 'client_factory_server.dart'
    if (dart.library.js_interop) 'client_factory_web.dart';

lib/mqtt/client_factory_server.dart :

import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

MqttClient createClient(String host, String clientId) {
  final client = MqttServerClient.withPort(host, clientId, 8883);
  client.secure = true; // TLS, validated against the system trust store
  return client;
}

lib/mqtt/client_factory_web.dart :

import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_browser_client.dart';

MqttClient createClient(String host, String clientId) {
  final client = MqttBrowserClient.withPort('wss://$host/mqtt', clientId, 8084);
  client.websocketProtocols = MqttClientConstants.protocolsSingleDefault;
  return client;
}

lib/mqtt/mqtt_service.dart :

import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:mqtt_client/mqtt_client.dart';
import 'client_factory.dart';

class MqttService {
  MqttService({
    this.host = 'broker.connectmq.com',
    this.username,
    this.password,
    String? clientId,
    String? topicPrefix,
  })  : clientId = clientId ?? 'flutter-${Random().nextInt(1000000000)}',
        topicPrefix = topicPrefix ?? 'connectmq/demo/${Random().nextInt(1000000000)}';

  final String host;
  final String? username;
  final String? password;
  final String clientId;
  final String topicPrefix;

  late final MqttClient _client;
  final _temperature = StreamController<double>.broadcast();
  final _connected = StreamController<bool>.broadcast();

  Stream<double> get temperature => _temperature.stream;
  Stream<bool> get connected => _connected.stream;

  String get sensorTopic => '$topicPrefix/sensor/temperature';
  String get relayTopic => '$topicPrefix/relay/set';

  Future<void> connect() async {
    _client = createClient(host, clientId);
    _client.keepAlivePeriod = 30;
    _client.autoReconnect = true;
    _client.resubscribeOnAutoReconnect = true;
    _client.logging(on: false);

    _client.onConnected = () => _connected.add(true);
    _client.onDisconnected = () => _connected.add(false);
    _client.onAutoReconnected = () => _connected.add(true);

    final connMessage = MqttConnectMessage()
        .withClientIdentifier(clientId)
        .startClean();
    if (username != null && password != null) {
      connMessage.authenticateAs(username!, password!);
    }
    _client.connectionMessage = connMessage;

    try {
      await _client.connect();
    } on Exception {
      _client.disconnect();
      rethrow;
    }

    if (_client.connectionStatus?.state != MqttConnectionState.connected) {
      throw StateError('MQTT connect failed: ${_client.connectionStatus}');
    }

    _client.subscribe(sensorTopic, MqttQos.atLeastOnce);
    _client.updates!.listen(_onMessages);
  }

  void _onMessages(List<MqttReceivedMessage<MqttMessage?>> messages) {
    for (final m in messages) {
      final publish = m.payload as MqttPublishMessage;
      final text = MqttPublishPayload.bytesToStringAsString(publish.payload.message);
      try {
        final value = (jsonDecode(text)['value'] as num).toDouble();
        _temperature.add(value);
      } catch (_) {
        // Ignore malformed payloads instead of crashing the stream.
      }
    }
  }

  void setRelay(bool on) {
    final builder = MqttClientPayloadBuilder()..addString(on ? 'ON' : 'OFF');
    _client.publishMessage(relayTopic, MqttQos.atLeastOnce, builder.payload!);
  }

  void dispose() {
    _client.disconnect();
    _temperature.close();
    _connected.close();
  }
}

Quelques détails comptent ici. autoReconnect associé à resubscribeOnAutoReconnect garantit qu'un réseau mobile instable ne vous laisse pas connecté mais sourd. startClean() convient à un client d'interface qui ne s'intéresse qu'aux données en direct ; un client qui ne doit manquer aucune commande pendant qu'il est hors ligne utiliserait plutôt une session persistante (voir les sessions persistantes MQTT). L'analyse JSON est protégée, car n'importe qui peut publier sur un topic public.

L'interface Flutter

lib/main.dart :

import 'package:flutter/material.dart';
import 'mqtt/mqtt_service.dart';

void main() => runApp(const MaterialApp(home: DeviceScreen()));

class DeviceScreen extends StatefulWidget {
  const DeviceScreen({super.key});
  @override
  State<DeviceScreen> createState() => _DeviceScreenState();
}

class _DeviceScreenState extends State<DeviceScreen> {
  final mqtt = MqttService();
  bool relayOn = false;
  String? error;

  @override
  void initState() {
    super.initState();
    mqtt.connect().catchError((e) => setState(() => error = '$e'));
  }

  @override
  void dispose() {
    mqtt.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter MQTT Demo')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            StreamBuilder<bool>(
              stream: mqtt.connected,
              initialData: false,
              builder: (_, s) => Chip(
                label: Text(s.data! ? 'Connected' : 'Disconnected'),
              ),
            ),
            if (error != null) Text(error!, style: const TextStyle(color: Colors.red)),
            const SizedBox(height: 24),
            StreamBuilder<double>(
              stream: mqtt.temperature,
              builder: (_, s) => Text(
                s.hasData ? '${s.data!.toStringAsFixed(1)} °C' : '-- °C',
                style: Theme.of(context).textTheme.displayMedium,
              ),
            ),
            const SizedBox(height: 24),
            SwitchListTile(
              title: const Text('Relay'),
              value: relayOn,
              onChanged: (v) {
                setState(() => relayOn = v);
                mqtt.setRelay(v);
              },
            ),
            const SizedBox(height: 24),
            SelectableText('Topic prefix: ${mqtt.topicPrefix}'),
          ],
        ),
      ),
    );
  }
}

Le préfixe de topic est affiché à l'écran pour que vous puissiez le copier dans un terminal lors des tests. Si vous hésitez entre Flutter et des stacks natives ou JavaScript, comparez avec les versions de la même application en client MQTT iOS en Swift et en React Native MQTT.

Tester avec un appareil simulé

Nul besoin de matériel pour l'instant. Ouvrez deux terminaux et jouez le rôle de l'appareil avec les outils en ligne de commande Mosquitto. Remplacez PREFIX par la valeur affichée dans l'application :

# Watch the commands the app sends
mosquitto_sub -h broker.connectmq.com -p 1883 -i sim-sub-81723 -t "PREFIX/relay/set" -v

# Publish a fake temperature reading every 2 seconds
while true; do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i sim-pub-81723 \
    -t "PREFIX/sensor/temperature" -m "{\"value\": $((RANDOM % 10 + 18)).$((RANDOM % 10))}"
  sleep 2
done

Basculez l'interrupteur dans l'application et vous devriez voir ON ou OFF arriver dans le terminal du subscriber. Si vous branchez plus tard une vraie carte, le tutoriel MQTT ESP32 utilise le même schéma de topics.

Structurer les topics Flutter MQTT pour davantage d'appareils

Un seul appareil, c'est facile. Lorsque l'application évolue vers une liste d'appareils, évitez de créer un abonnement par widget d'appareil. Abonnez-vous une seule fois à un wildcard comme <prefix>/+/sensor/temperature, extrayez l'ID de l'appareil du topic dans _onMessage et émettez une map indexée par appareil au lieu d'une valeur unique. Le nombre d'abonnements reste ainsi constant, ce qui compte sur les brokers partagés avec des limites par client ; le broker public autorise 10 abonnements par client.

Deux autres habitudes sont payantes. Faites publier aux appareils leur état courant avec le flag retain, pour qu'une application fraîchement ouverte affiche immédiatement l'état réel du relais plutôt qu'un false par défaut ; le guide des retained messages MQTT explique comment faire. Et laissez l'appareil confirmer les commandes sur un topic d'état distinct comme relay/state au lieu de vous fier à la position de l'interrupteur. L'interface doit refléter ce que l'appareil signale, pas ce que l'utilisateur a touché : si une commande est perdue, l'interrupteur revient à l'état réel au rapport suivant.

Passer du broker de test à la production

Le broker public sert aux expériences. Pour un vrai produit, passez à un broker MQTT managé privé afin que seuls vos appareils et vos applications puissent s'y connecter :

  1. Inscrivez-vous sur le portail ConnectMQ, choisissez un plan et créez un broker.
  2. Copiez le nom d'utilisateur, le mot de passe et le client ID générés.
  3. Construisez le service avec ces valeurs : MqttService(username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', clientId: 'YOUR_CLIENT_ID', topicPrefix: 'acme/site1').
  4. Conservez TLS sur le port 8883 (ou WSS sur 8084 pour le web).

Sur ConnectMQ, les client ID sont liés aux identifiants émis : chaque installation de l'application qui se connecte en même temps a donc besoin de sa propre identité. N'embarquez pas un mot de passe partagé dans chaque copie d'une application publique ; émettez des identifiants par utilisateur ou par appareil depuis un backend que vous contrôlez, et considérez que tout ce qui est intégré dans un binaire mobile peut être extrait.

Pièges par plateforme et dépannage

Symptôme Cause probable Solution
Le build release Android ne se connecte pas, le debug fonctionne Permission Internet manquante Ajoutez <uses-permission android:name="android.permission.INTERNET"/> dans le AndroidManifest.xml principal
L'application macOS échoue avec « Operation not permitted » Le sandbox bloque le réseau sortant Activez com.apple.security.network.client dans les fichiers d'entitlements
Le build web ne se connecte jamais Utilisation de MqttServerClient ou de ws:// sur une page HTTPS Utilisez MqttBrowserClient avec wss:// sur 8084 et le chemin /mqtt
Se connecte, puis décroche toutes les minutes Keep-alive plus long que le délai d'inactivité du NAT Réduisez keepAlivePeriod à 20 à 30 secondes
Les messages s'arrêtent à la reprise de l'application Socket tué en arrière-plan, pas de réabonnement Laissez autoReconnect et resubscribeOnAutoReconnect activés
Connexion refusée, « identifier rejected » Client ID vide ou en double Utilisez un client ID unique et non vide par session

Les systèmes d'exploitation mobiles suspendent agressivement les applications en arrière-plan. N'essayez pas de maintenir une connexion MQTT ouverte pendant que l'application est en arrière-plan sur iOS ; reconnectez-vous lorsque l'application revient au premier plan et utilisez les notifications push pour les alertes qui doivent atteindre un téléphone en veille. Le guide de l'intervalle keep-alive MQTT explique comment ajuster les délais pour les réseaux cellulaires.

Si vous devez voir ce qui transite réellement, le portail ConnectMQ inclut un client MQTT intégré au navigateur et un explorateur de topics en direct : c'est le moyen le plus rapide de savoir si un problème vient de votre application ou de l'appareil.

Démarrer avec ConnectMQ

Votre application Flutter est prête pour un vrai broker. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, copiez les identifiants générés dans MqttService, et vos builds Android, iOS, desktop et web partageront tous le même endpoint sécurisé. Lorsque vous dépasserez l'offre gratuite, le même code fonctionnera sur les plans payants et dédiés.

Questions fréquentes

Flutter MQTT fonctionne-t-il sur Flutter web ?

Oui, mais uniquement via WebSocket. Utilisez MqttBrowserClient avec wss://broker.connectmq.com/mqtt sur le port 8084 ; les navigateurs ne peuvent pas ouvrir de connexions TCP brutes vers les ports 1883 ou 8883.

Le package mqtt_client prend-il en charge MQTT 5 ?

Non, mqtt_client implémente MQTT 3.1.1. Utilisez le package jumeau mqtt5_client si vous avez besoin de fonctionnalités MQTT 5 comme les user properties ou l'expiration des messages.

Peut-on garder une connexion MQTT active pendant que l'application Flutter est en arrière-plan ?

Pas de manière fiable sur mobile, car iOS et Android suspendent les sockets en arrière-plan. Reconnectez-vous à la reprise et utilisez les notifications push pour les alertes qui doivent atteindre les utilisateurs lorsque l'application est fermée.

Quelle QoS une application Flutter doit-elle utiliser ?

QoS 1 est un bon choix par défaut pour les commandes et les relevés que vous ne voulez pas perdre. Le plan Serverless gratuit de ConnectMQ prend en charge la QoS jusqu'à 1, et les plans payants ajoutent la QoS 2.

Articles liés

Les meilleurs outils CLI MQTT pour tester, scripter et déboguer

Un comparatif pratique de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI MQTT.js, curl et mqttui, avec le même test exécuté sur le serveur MQTT public gratuit broker.connectmq.com pour choisir le bon outil pour le scripting et le débogage.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.