Tutorials

MQTT in Flutter mit mqtt_client: Mobile, Desktop und Web

Flutter-App mit mqtt_client an einen MQTT-Broker anbinden: Server- vs. Browser-Client, TLS auf 8883, sichere WebSockets für Flutter Web, Reconnect und Tests.

Aktualisiert · 9 Min. Lesezeit

MQTT in Flutter mit mqtt_client: Mobile, Desktop und Web

Um MQTT in Flutter zu nutzen, fügst du das Paket mqtt_client hinzu, erzeugst einen MqttServerClient für Android, iOS und Desktop oder einen MqttBrowserClient für Flutter Web, verbindest dich, abonnierst und hörst auf den Stream updates. Die häufigste Stolperfalle: Die beiden Clients nutzen unterschiedliche Transporte. Der Server-Client spricht TCP oder TLS, der Browser-Client kann ausschließlich WebSockets.

Dieses Tutorial baut einen kleinen, wiederverwendbaren MQTT-Service für Flutter, zeigt die Konfigurationen für TCP, TLS und sichere WebSockets gegen broker.connectmq.com und endet mit einem schnellen Weg, den kompletten Ablauf im Browser zu testen.

mqtt_client installieren

mqtt_client ist ein reines Dart-Paket und läuft deshalb ohne native Plugins auf jeder Flutter-Plattform:

Abhängigkeit hinzufügen
flutter pub add mqtt_client

Das Paket enthält zwei Client-Klassen in zwei getrennten Libraries. Welche du importierst, bestimmt den Transport:

KlasseImportPlattformenTransporte
MqttServerClientpackage:mqtt_client/mqtt_server_client.dartAndroid, iOS, macOS, Windows, LinuxTCP, TLS, WebSocket, sicherer WebSocket
MqttBrowserClientpackage:mqtt_client/mqtt_browser_client.dartFlutter WebNur WebSocket und sicherer WebSocket

mqtt_server_client.dart importiert dart:io, das es im Browser nicht gibt; ein Web-Build mit diesem Import scheitert also beim Kompilieren. Weiter unten lösen wir das mit einem bedingten Import. Schreibst du statt einer App ein reines Dart-Kommandozeilentool, gilt dieselbe API – siehe MQTT in Dart.

Erste Verbindung über TCP

Starte mit dem einfachsten Setup: unverschlüsseltes TCP auf Port 1883. Jeder Client braucht eine eindeutige Client-ID; teilen sich zwei Geräte eine, trennt der Broker jedes Mal die ältere Session, wenn sich das andere verbindet. Der Client-ID-Generator hilft beim 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; // Millisekunden
  client.onConnected = () => print('verbunden');
  client.onDisconnected = () => print('getrennt');

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

  try {
    await client.connect(); // oder client.connect('username', 'password')
  } on Exception catch (e) {
    print('Verbindung fehlgeschlagen: $e');
    client.disconnect();
    rethrow;
  }

  final state = client.connectionStatus?.state;
  if (state != MqttConnectionState.connected) {
    throw Exception('Broker hat die Verbindung abgelehnt: ${client.connectionStatus}');
  }
  return client;
}

connect() wirft eine NoConnectionException, wenn der Broker das CONNECT ablehnt (falsche Zugangsdaten, gesperrte Client-ID), und eine SocketException, wenn Host oder Port nicht erreichbar sind. client.connectionStatus liefert den Return Code, den du in der Tabelle der MQTT Reason Codes nachschlagen kannst.

Abonnieren und Nachrichten empfangen

Eingehende Nachrichten landen in client.updates, einem Dart-Stream. Erst abonnieren, dann einmal zuhören – nicht einmal pro Subscription:

Abonnieren und zuhören
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 dekodiert die Bytes als Text. Für JSON gibst du das Ergebnis an jsonDecode weiter; für binäre Payloads (CBOR, Protobuf) nutzt du direkt publish.payload.message, einen typisierten Byte-Puffer.

In einem Widget speicherst du die von listen zurückgegebene StreamSubscription und kündigst sie in dispose(), oder du stellst den Stream über dein State Management (Provider, Riverpod, Bloc) bereit. So bauen sich die Widgets aus einer einzigen Quelle neu auf, statt dass jedes eine eigene Verbindung öffnet.

Veröffentlichen

Payloads baust du mit MqttClientPayloadBuilder:

Einen JSON-Messwert veröffentlichen
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,
  );
}

Nimm addUTF8String statt addString, sobald Text Nicht-ASCII-Zeichen enthalten kann; addString schreibt ein Byte pro UTF-16-Code-Unit und kann Umlaute und Emojis verstümmeln.

TLS auf Port 8883

Für die Produktion stellst du den Server-Client auf TLS um. Broker mit einem Zertifikat einer öffentlichen CA wie broker.connectmq.com werden gegen den Trust Store des Betriebssystems geprüft, zusätzliche Dateien brauchst du nicht:

TLS-Konfiguration
import 'dart:io';

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

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

Bei einem Broker mit selbstsigniertem Zertifikat oder privater CA erzeugst du einen SecurityContext(withTrustedRoots: false) und rufst setTrustedCertificatesBytes() mit dem CA-Zertifikat aus deinen Assets auf. Verzichte außerhalb kurzer lokaler Experimente auf onBadCertificate: (_) => true – das akzeptiert jedes Zertifikat und hebelt TLS stillschweigend aus. Schlägt der Handshake fehl, zeigt dir ein Test der TLS-Verbindung im Terminal, ob das Problem beim Broker oder bei der App liegt.

Flutter Web: MqttBrowserClient über WSS

Im Browser läuft MQTT über WebSockets. Per HTTPS ausgelieferte Seiten dürfen nur wss://-Verbindungen öffnen, also nutzt du den sicheren WebSocket-Port und gibst den Pfad mit an:

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;
}

Der Port wird als Property gesetzt, nicht in der URL. Die Zeile websocketProtocols schickt den Subprotokoll-Header mqtt, den die meisten Broker erwarten. Ein falscher Pfad zeigt sich meist als WebSocket-Close direkt nach dem Upgrade; MQTT über WebSockets erklärt den Handshake im Detail.

Eine Codebasis für alle Plattformen

Lege ein passendes lib/mqtt_server.dart an, das einen MqttServerClient zurückgibt (zum Beispiel auf 8883 mit TLS), und wähle die richtige Datei beim Kompilieren:

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

Der Rest der App importiert nur mqtt_factory.dart und package:mqtt_client/mqtt_client.dart und arbeitet mit der gemeinsamen Basisklasse MqttClient. Connect, Subscribe, Publish und der Stream updates verhalten sich auf beiden gleich.

Reconnect und App-Lebenszyklus

Mobilfunknetze trennen Verbindungen ständig: Wechsel von WLAN auf Mobilfunk, Tunnel, das Betriebssystem pausiert deine App. mqtt_client bringt einen eingebauten Auto-Reconnect mit:

Auto-Reconnect
client
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true
  ..onAutoReconnect = () => print('Verbindung verloren, verbinde neu...')
  ..onAutoReconnected = () => print('wieder verbunden');

Mit resubscribeOnAutoReconnect (Standard: true) spielt der Client deine Subscriptions nach dem Reconnect erneut ein. Das ist wichtig, weil eine Clean Session sie auf Broker-Seite vergisst. Nachrichten, die während der Trennung veröffentlicht werden, hebt der Broker bei einer Clean Session nicht auf. Darf die App keine Daten verpassen, verbinde dich mit einer persistenten Session (ohne startClean(), mit stabiler Client-ID) und abonniere mit QoS 1. Teste das gegen deine eigene private Broker-Instanz: Der geteilte Test-Endpoint hält keine Offline-Sessions vor.

Denk daran, dass iOS und Android Apps im Hintergrund pausieren und dabei der Socket unabhängig vom Keep-Alive geschlossen wird. Beobachte den Lebenszyklus mit WidgetsBindingObserver, trenne in paused sauber und verbinde in resumed neu. Für Zustellung im Hintergrund sind Push-Benachrichtigungen das richtige Werkzeug; siehe Hintergrund-Disconnects unter Android und Hintergrund-Limits unter iOS.

Checkliste pro Plattform

  • Android: Füge <uses-permission android:name="android.permission.INTERNET"/> in android/app/src/main/AndroidManifest.xml ein. Debug-Builds enthalten die Berechtigung automatisch – deshalb ist „läuft in Debug, scheitert in Release“ so verbreitet.
  • macOS: Setze com.apple.security.network.client auf true, sowohl in DebugProfile.entitlements als auch in Release.entitlements, sonst blockiert die Sandbox ausgehende Sockets.
  • iOS: Für TLS nichts Zusätzliches. App Transport Security gilt für URL-Loading-APIs, nicht für rohes TCP oder ws:// zu einem Hostnamen – in der Produktion solltest du trotzdem TLS verwenden.
  • Web: MqttBrowserClient, wss://, richtiger Port und Pfad. Mixed Content (ein ws://-Broker von einer https://-Seite) blockiert der Browser.

Den kompletten Ablauf testen

Starte die App, öffne den Online-MQTT-Client im Browser, verbinde dich per sicherem WebSocket mit broker.connectmq.com und abonniere testmqtt/flutter-demo/#. Du solltest die Messwerte sehen, die deine App veröffentlicht, und was du aus dem Browser an testmqtt/flutter-demo/sensors/test sendest, sollte in der Debug-Konsole von Flutter erscheinen.

Nachrichten deiner Flutter-App im Browser ansehen

Kommt nichts an, prüfe in dieser Reihenfolge: den Verbindungsstatus nach connect(), ob das abonnierte Topic zum veröffentlichten passt (der Topic Matcher hilft bei Wildcards) und die Debug-Ausgabe nach client.logging(on: true). Die häufigsten Exceptions und ihre Lösungen findest du unter Flutter-MQTT-Fehler.

Mit einer Factory-Datei, einem einzigen Nachrichten-Stream und aktivem Auto-Reconnect spricht derselbe Flutter-Code vom Smartphone, vom Desktop und aus einem Browser-Tab mit dem Broker. Starte mit dem geteilten Endpoint, wechsle auf TLS und sichere WebSockets mit Zugangsdaten und lass den Browser-Client während der Entwicklung offen, damit du jede Nachricht deiner App siehst.

Häufig gestellte Fragen

Warum läuft meine Flutter-MQTT-App auf Android, aber nicht im Web?

Browser können keine rohen TCP-Sockets öffnen, deshalb funktioniert MqttServerClient in Flutter Web nicht. Nutze MqttBrowserClient mit einer wss://-URL, dem WebSocket-Port des Brokers (zum Beispiel 8084) und dem richtigen Pfad wie /mqtt.

Unterstützt mqtt_client MQTT 5?

Nein. mqtt_client implementiert MQTT 3.1 und 3.1.1. Brauchst du MQTT-5-Funktionen wie Reason Codes, User Properties oder Session Expiry, nimm das separate Paket mqtt5_client desselben Autors, das eine sehr ähnliche API hat.

Brauche ich besondere Berechtigungen für MQTT in Flutter?

Unter Android brauchen Release-Builds die INTERNET-Berechtigung in der AndroidManifest.xml (Debug-Builds fügen sie automatisch hinzu). Unter macOS aktivierst du das Entitlement für ausgehende Verbindungen (com.apple.security.network.client). iOS und Web brauchen nichts weiter.