Tutoriels

MQTT avec Flutter et mqtt_client : mobile, desktop et web

Connectez une app Flutter à un broker MQTT avec mqtt_client : client server ou browser, TLS sur 8883, WebSockets sécurisés pour Flutter web, reconnexion, tests.

Mis à jour · 9 min de lecture

MQTT avec Flutter et mqtt_client : mobile, desktop et web

Pour utiliser MQTT avec Flutter, ajoutez le package mqtt_client, créez un MqttServerClient pour Android, iOS et desktop ou un MqttBrowserClient pour Flutter web, connectez-vous, abonnez-vous et écoutez le stream updates. Le piège le plus courant : les deux clients n'utilisent pas le même transport. Le client server parle TCP ou TLS, tandis que le client browser ne peut utiliser que les WebSockets.

Ce tutoriel construit un petit service MQTT réutilisable pour Flutter, montre les configurations TCP, TLS et WebSocket sécurisé vers broker.connectmq.com, et se termine par un moyen rapide de tester l'aller-retour complet depuis votre navigateur.

Installer mqtt_client

mqtt_client est un package Dart pur : il fonctionne donc sur toutes les plateformes Flutter sans plugin natif.

Ajouter la dépendance
flutter pub add mqtt_client

Le package contient deux classes de client dans deux bibliothèques distinctes. Celle que vous importez détermine le transport :

ClasseImportPlateformesTransports
MqttServerClientpackage:mqtt_client/mqtt_server_client.dartAndroid, iOS, macOS, Windows, LinuxTCP, TLS, WebSocket, WebSocket sécurisé
MqttBrowserClientpackage:mqtt_client/mqtt_browser_client.dartFlutter webWebSocket et WebSocket sécurisé uniquement

mqtt_server_client.dart importe dart:io, qui n'existe pas dans le navigateur ; l'importer dans un build web échoue donc à la compilation. Nous réglerons cela plus bas avec un import conditionnel. Si vous écrivez un outil en ligne de commande en Dart pur plutôt qu'une app, la même API s'applique : voir MQTT en Dart.

Première connexion en TCP

Commencez par la configuration la plus simple : TCP non chiffré sur le port 1883. Chaque client a besoin d'un client ID unique ; si deux appareils partagent le même, le broker déconnecte la session la plus ancienne chaque fois que l'autre se connecte. Le générateur de client ID aide à choisir un format.

lib/mqtt_demo.dart
import 'dart:math';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

Future<MqttServerClient> connectMqtt() async {
  final clientId = 'flutter-${Random().nextInt(0xFFFFFF).toRadixString(16)}';
  final client = MqttServerClient.withPort('broker.connectmq.com', clientId, 1883);

  client.logging(on: false);
  client.setProtocolV311();
  client.keepAlivePeriod = 30;
  client.connectTimeoutPeriod = 5000; // millisecondes
  client.onConnected = () => print('connecté');
  client.onDisconnected = () => print('déconnecté');

  client.connectionMessage = MqttConnectMessage()
      .withClientIdentifier(clientId)
      .startClean()
      .withWillTopic('testmqtt/flutter-demo/$clientId/status')
      .withWillMessage('offline')
      .withWillQos(MqttQos.atLeastOnce);

  try {
    await client.connect(); // ou client.connect('username', 'password')
  } on Exception catch (e) {
    print('échec de connexion : $e');
    client.disconnect();
    rethrow;
  }

  final state = client.connectionStatus?.state;
  if (state != MqttConnectionState.connected) {
    throw Exception('le broker a refusé la connexion : ${client.connectionStatus}');
  }
  return client;
}

connect() lève une NoConnectionException quand le broker rejette le CONNECT (identifiants incorrects, client ID refusé) et une SocketException quand l'hôte ou le port est injoignable. Afficher client.connectionStatus donne le code de retour, à rechercher dans le tableau des reason codes MQTT.

S'abonner et recevoir des messages

Les messages entrants arrivent sur client.updates, un stream Dart. Abonnez-vous d'abord, puis écoutez une seule fois, et non une fois par abonnement :

S'abonner et écouter
const topic = 'testmqtt/flutter-demo/sensors/#';

void listen(MqttServerClient client) {
  client.subscribe(topic, MqttQos.atLeastOnce);

  client.updates!.listen((messages) {
    for (final m in messages) {
      final publish = m.payload as MqttPublishMessage;
      final text = MqttPublishPayload.bytesToStringAsString(publish.payload.message);
      print('${m.topic}: $text');
    }
  });
}

bytesToStringAsString décode les octets en texte. Pour du JSON, passez le résultat à jsonDecode ; pour des payloads binaires (CBOR, Protobuf), utilisez directement publish.payload.message, un buffer d'octets typé.

Dans un widget, conservez la StreamSubscription renvoyée par listen et annulez-la dans dispose(), ou exposez le stream via votre solution de gestion d'état (Provider, Riverpod, Bloc) : les widgets se reconstruisent ainsi à partir d'une source unique au lieu d'ouvrir chacun leur propre connexion.

Publier

Les payloads se construisent avec MqttClientPayloadBuilder :

Publier une mesure JSON
import 'dart:convert';

void publishReading(MqttServerClient client, double celsius) {
  final builder = MqttClientPayloadBuilder()
    ..addUTF8String(jsonEncode({'celsius': celsius, 'ts': DateTime.now().toIso8601String()}));

  client.publishMessage(
    'testmqtt/flutter-demo/sensors/kitchen',
    MqttQos.atLeastOnce,
    builder.payload!,
    retain: false,
  );
}

Préférez addUTF8String à addString dès que le texte peut contenir des caractères non ASCII : addString écrit un octet par unité de code UTF-16 et peut abîmer les accents et les emojis.

TLS sur le port 8883

En production, passez le client server en TLS. Les brokers dotés d'un certificat d'une CA publique, comme broker.connectmq.com, sont vérifiés via le magasin de confiance du système, sans fichier supplémentaire :

Configuration TLS
import 'dart:io';

final client = MqttServerClient.withPort('broker.connectmq.com', clientId, 8883)
  ..secure = true
  ..securityContext = SecurityContext.defaultContext;

await client.connect('username', 'password');

Pour un broker avec un certificat auto-signé ou une CA privée, créez un SecurityContext(withTrustedRoots: false) et appelez setTrustedCertificatesBytes() avec le certificat de la CA chargé depuis vos assets. Évitez onBadCertificate: (_) => true en dehors d'une expérience locale rapide : il accepte tous les certificats et annule TLS sans prévenir. Si le handshake échoue, tester la connexion TLS depuis un terminal indique si le problème vient du broker ou de l'app.

Flutter web : MqttBrowserClient en WSS

Dans le navigateur, MQTT passe par les WebSockets. Les pages servies en HTTPS ne peuvent ouvrir que des connexions wss:// : utilisez donc le port WebSocket sécurisé et indiquez le chemin.

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

MqttClient createClient(String clientId) {
  final client = MqttBrowserClient('wss://broker.connectmq.com/mqtt', clientId)
    ..port = 8084
    ..keepAlivePeriod = 30
    ..websocketProtocols = MqttClientConstants.protocolsSingleDefault;
  return client;
}

Notez que le port est défini comme propriété, pas dans l'URL. La ligne websocketProtocols envoie l'en-tête de sous-protocole mqtt, attendu par la plupart des brokers. Un mauvais chemin se manifeste généralement par une fermeture du WebSocket juste après l'upgrade ; MQTT sur WebSockets détaille le handshake.

Un seul code pour toutes les plateformes

Créez un lib/mqtt_server.dart équivalent qui renvoie un MqttServerClient (par exemple sur 8883 avec TLS), puis choisissez le bon fichier à la compilation :

lib/mqtt_factory.dart
export 'mqtt_server.dart'
    if (dart.library.js_interop) 'mqtt_browser.dart';

Le reste de l'app n'importe que mqtt_factory.dart et package:mqtt_client/mqtt_client.dart, et travaille avec la classe de base commune MqttClient. Connect, subscribe, publish et le stream updates se comportent de la même façon des deux côtés.

Reconnexion et cycle de vie de l'app

Les réseaux mobiles coupent sans cesse les connexions : passage du Wi-Fi au réseau mobile, tunnels, système qui suspend votre app. mqtt_client intègre une reconnexion automatique :

Reconnexion automatique
client
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true
  ..onAutoReconnect = () => print('connexion perdue, reconnexion...')
  ..onAutoReconnected = () => print('reconnecté');

Avec resubscribeOnAutoReconnect (true par défaut), le client rejoue vos abonnements après la reconnexion, ce qui compte car une clean session les oublie côté broker. Avec une clean session, le broker ne conserve pas les messages publiés pendant la coupure ; si l'app ne doit rien manquer, connectez-vous avec une session persistante (sans startClean(), avec un client ID stable) et abonnez-vous en QoS 1. Testez cela sur votre propre instance privée de broker : l'endpoint de test partagé ne conserve pas les sessions hors ligne.

N'oubliez pas qu'iOS et Android suspendent les apps en arrière-plan, ce qui ferme le socket quel que soit le keep-alive. Observez le cycle de vie avec WidgetsBindingObserver, déconnectez proprement dans paused et reconnectez dans resumed. Pour la livraison en arrière-plan, les notifications push sont le bon outil ; voir déconnexions en arrière-plan sur Android et limites d'arrière-plan sur iOS.

Checklist par plateforme

  • Android : ajoutez <uses-permission android:name="android.permission.INTERNET"/> dans android/app/src/main/AndroidManifest.xml. Les builds debug l'incluent automatiquement, d'où le fameux « ça marche en debug, pas en release ».
  • macOS : ajoutez com.apple.security.network.client à true dans DebugProfile.entitlements et Release.entitlements, sinon le sandbox bloque les sockets sortants.
  • iOS : rien de plus pour TLS. App Transport Security concerne les API de chargement d'URL, pas le TCP brut ni ws:// vers un nom d'hôte, mais utilisez quand même TLS en production.
  • Web : MqttBrowserClient, wss://, le bon port et le bon chemin. Le navigateur bloque le contenu mixte (un broker ws:// depuis une page https://).

Tester l'aller-retour

Lancez l'app, ouvrez le client MQTT en ligne dans un navigateur, connectez-vous à broker.connectmq.com en WebSocket sécurisé et abonnez-vous à testmqtt/flutter-demo/#. Vous devez voir les mesures publiées par votre app, et tout ce que vous publiez depuis le navigateur sur testmqtt/flutter-demo/sensors/test doit apparaître dans la console de debug Flutter.

Voir les messages de votre app Flutter dans le navigateur

Si rien n'arrive, vérifiez dans cet ordre : l'état de connexion après connect(), la correspondance entre le topic d'abonnement et celui de publication (le topic matcher aide avec les wildcards) et la sortie de debug après client.logging(on: true). Les exceptions les plus fréquentes et leurs solutions sont réunies dans erreurs MQTT Flutter.

Avec un fichier factory, un seul stream de messages et la reconnexion automatique activée, le même code Flutter dialogue avec le broker depuis un téléphone, un ordinateur et un onglet de navigateur. Commencez avec l'endpoint partagé, passez à TLS et aux WebSockets sécurisés avec identifiants, et gardez le client navigateur ouvert pendant le développement pour voir chaque message envoyé par votre app.

Questions fréquentes

Pourquoi mon app Flutter MQTT fonctionne sur Android mais pas sur le web ?

Les navigateurs ne peuvent pas ouvrir de sockets TCP bruts, donc MqttServerClient ne fonctionne pas dans Flutter web. Utilisez MqttBrowserClient avec une URL wss://, le port WebSocket du broker (par exemple 8084) et le bon chemin, comme /mqtt.

mqtt_client prend-il en charge MQTT 5 ?

Non. mqtt_client implémente MQTT 3.1 et 3.1.1. Si vous avez besoin de fonctionnalités MQTT 5 comme les reason codes, les user properties ou le session expiry, utilisez le package mqtt5_client du même auteur, dont l'API est très proche.

Faut-il des permissions particulières pour utiliser MQTT dans Flutter ?

Sur Android, les builds release ont besoin de la permission INTERNET dans AndroidManifest.xml (les builds debug l'ajoutent automatiquement). Sur macOS, activez l'entitlement des connexions sortantes (com.apple.security.network.client). iOS et le web n'ont besoin de rien.