Tutoriales

MQTT en Flutter con mqtt_client: móvil, escritorio y web

Conecta una app Flutter a un broker MQTT con mqtt_client: cliente server vs browser, TLS en 8883, WebSockets seguros para Flutter web, reconexión y pruebas.

Actualizado · 9 min de lectura

MQTT en Flutter con mqtt_client: móvil, escritorio y web

Para usar MQTT en Flutter, añade el paquete mqtt_client, crea un MqttServerClient para Android, iOS y escritorio o un MqttBrowserClient para Flutter web, conéctate, suscríbete y escucha el stream updates. Lo que más confunde es que los dos clientes usan transportes distintos: el cliente server habla TCP o TLS, mientras que el cliente browser solo puede usar WebSockets.

Este tutorial construye un pequeño servicio MQTT reutilizable para Flutter, muestra las configuraciones TCP, TLS y WebSocket seguro contra broker.connectmq.com y termina con una forma rápida de probar el recorrido completo desde tu navegador.

Instalar mqtt_client

mqtt_client es un paquete Dart puro, así que funciona en todas las plataformas de Flutter sin plugins nativos:

Añadir la dependencia
flutter pub add mqtt_client

El paquete contiene dos clases de cliente en dos librerías separadas. La que importes decide qué transporte obtienes:

ClaseImportPlataformasTransportes
MqttServerClientpackage:mqtt_client/mqtt_server_client.dartAndroid, iOS, macOS, Windows, LinuxTCP, TLS, WebSocket, WebSocket seguro
MqttBrowserClientpackage:mqtt_client/mqtt_browser_client.dartFlutter webSolo WebSocket y WebSocket seguro

mqtt_server_client.dart importa dart:io, que no existe en el navegador, por lo que importarlo en una build web falla al compilar. Lo resolveremos más abajo con un import condicional. Si escribes una herramienta de línea de comandos en Dart puro en lugar de una app, se aplica la misma API: consulta MQTT en Dart.

Primera conexión por TCP

Empieza con la configuración más simple: TCP sin cifrar en el puerto 1883. Cada cliente necesita un client ID único; si dos dispositivos comparten uno, el broker desconecta la sesión anterior cada vez que el otro se conecta. El generador de client ID ayuda a elegir un formato.

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; // milisegundos
  client.onConnected = () => print('conectado');
  client.onDisconnected = () => print('desconectado');

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

  try {
    await client.connect(); // o client.connect('username', 'password')
  } on Exception catch (e) {
    print('fallo al conectar: $e');
    client.disconnect();
    rethrow;
  }

  final state = client.connectionStatus?.state;
  if (state != MqttConnectionState.connected) {
    throw Exception('el broker rechazó la conexión: ${client.connectionStatus}');
  }
  return client;
}

connect() lanza NoConnectionException cuando el broker rechaza el CONNECT (credenciales incorrectas, client ID bloqueado) y una SocketException cuando no se puede alcanzar el host o el puerto. Imprimir client.connectionStatus te da el código de retorno, que puedes buscar en la tabla de reason codes de MQTT.

Suscribirse y recibir mensajes

Los mensajes entrantes llegan a client.updates, un stream de Dart. Suscríbete primero y luego escucha una sola vez, no una vez por suscripción:

Suscribirse y escuchar
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 decodifica los bytes como texto. Para JSON, pasa el resultado a jsonDecode; para payloads binarios (CBOR, Protobuf) usa directamente publish.payload.message, que es un buffer de bytes tipado.

En un widget, guarda la StreamSubscription que devuelve listen y cancélala en dispose(), o expón el stream a través de tu solución de gestión de estado (Provider, Riverpod, Bloc) para que los widgets se reconstruyan desde una única fuente de verdad en lugar de abrir cada uno su propia conexión.

Publicar

Los payloads se construyen con MqttClientPayloadBuilder:

Publicar una lectura 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,
  );
}

Usa addUTF8String en lugar de addString cuando el texto pueda contener caracteres no ASCII; addString escribe un byte por cada unidad de código UTF-16 y puede estropear tildes, eñes y emojis.

TLS en el puerto 8883

Para producción, cambia el cliente server a TLS. Los brokers con un certificado de una CA pública, como broker.connectmq.com, se verifican con el almacén de confianza del sistema operativo, así que no necesitas archivos adicionales:

Configuración TLS
import 'dart:io';

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

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

Para un broker con certificado autofirmado o CA privada, crea un SecurityContext(withTrustedRoots: false) y llama a setTrustedCertificatesBytes() con el certificado de la CA cargado desde tus assets. Evita onBadCertificate: (_) => true fuera de un experimento local rápido: acepta cualquier certificado y anula TLS en silencio. Si el handshake falla, probar la conexión TLS desde una terminal te dice si el problema está en el broker o en la app.

Flutter web: MqttBrowserClient por WSS

En el navegador, MQTT funciona sobre WebSockets. Las páginas servidas por HTTPS solo pueden abrir conexiones wss://, así que usa el puerto WebSocket seguro e incluye la ruta:

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

Fíjate en que el puerto se define como propiedad, no dentro de la URL. La línea websocketProtocols envía la cabecera de subprotocolo mqtt, que la mayoría de brokers espera. Una ruta incorrecta suele aparecer como un cierre del WebSocket justo después del upgrade; MQTT sobre WebSockets explica el handshake en detalle.

Un solo código para todas las plataformas

Crea un lib/mqtt_server.dart equivalente que devuelva un MqttServerClient (por ejemplo en 8883 con TLS) y elige el archivo correcto en tiempo de compilación:

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

El resto de la app importa solo mqtt_factory.dart y package:mqtt_client/mqtt_client.dart, y trabaja con la clase base común MqttClient. Connect, subscribe, publish y el stream updates se comportan igual en ambos.

Reconexión y ciclo de vida de la app

Las redes móviles cortan conexiones constantemente: cambios de Wi-Fi a datos móviles, túneles, el sistema suspendiendo tu app. mqtt_client incluye reconexión automática:

Reconexión automática
client
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true
  ..onAutoReconnect = () => print('conexión perdida, reconectando...')
  ..onAutoReconnected = () => print('reconectado');

Con resubscribeOnAutoReconnect (por defecto true) el cliente repite tus suscripciones tras reconectar, lo cual importa porque una clean session las olvida en el lado del broker. El broker no guarda los mensajes publicados mientras estabas desconectado en una clean session, así que si la app no puede perder datos, conéctate con una sesión persistente (omite startClean(), mantén un client ID estable) y suscríbete con QoS 1. Pruébalo contra tu propia instancia privada de broker: el endpoint compartido de pruebas no guarda sesiones offline.

Recuerda que iOS y Android suspenden las apps en segundo plano, lo que cierra el socket sin importar el keep-alive. Observa el ciclo de vida con WidgetsBindingObserver, desconéctate limpiamente en paused y vuelve a conectar en resumed. Para entregas en segundo plano, la herramienta adecuada son las notificaciones push; consulta desconexiones en segundo plano en Android y límites en segundo plano en iOS.

Checklist por plataforma

  • Android: añade <uses-permission android:name="android.permission.INTERNET"/> a android/app/src/main/AndroidManifest.xml. Las builds de debug lo incluyen automáticamente, por eso "funciona en debug, falla en release" es tan habitual.
  • macOS: añade com.apple.security.network.client con valor true en DebugProfile.entitlements y en Release.entitlements, o el sandbox bloqueará los sockets salientes.
  • iOS: nada adicional para TLS. App Transport Security se aplica a las APIs de carga de URLs, no a TCP directo ni a ws:// hacia un hostname, pero aun así deberías usar TLS en producción.
  • Web: usa MqttBrowserClient, wss://, el puerto y la ruta correctos. El navegador bloquea el contenido mixto (un broker ws:// desde una página https://).

Probar el recorrido completo

Ejecuta la app y abre el cliente MQTT online en un navegador, conéctate a broker.connectmq.com por WebSocket seguro y suscríbete a testmqtt/flutter-demo/#. Deberías ver las lecturas que publica tu app, y lo que publiques desde el navegador en testmqtt/flutter-demo/sensors/test debería aparecer en la consola de debug de Flutter.

Mira en el navegador los mensajes de tu app Flutter

Si no llega nada, revisa en este orden: el estado de conexión después de connect(), que el topic de la suscripción coincida con el de publicación (el topic matcher ayuda con los wildcards) y la salida de debug tras client.logging(on: true). Las excepciones más frecuentes y sus soluciones están reunidas en errores MQTT en Flutter.

Con un archivo factory, un único stream de mensajes y la reconexión automática activada, el mismo código Flutter habla con el broker desde un móvil, un escritorio y una pestaña del navegador. Empieza con el endpoint compartido, pasa a TLS y WebSockets seguros con credenciales, y mantén abierto el cliente del navegador mientras desarrollas para ver cada mensaje que envía tu app.

Preguntas frecuentes

¿Por qué mi app Flutter con MQTT funciona en Android pero no en la web?

Los navegadores no pueden abrir sockets TCP directos, así que MqttServerClient no funciona en Flutter web. Usa MqttBrowserClient con una URL wss://, el puerto WebSocket del broker (por ejemplo 8084) y la ruta correcta, como /mqtt.

¿mqtt_client soporta MQTT 5?

No. mqtt_client implementa MQTT 3.1 y 3.1.1. Si necesitas funciones de MQTT 5 como reason codes, user properties o session expiry, usa el paquete mqtt5_client del mismo autor, que tiene una API muy parecida.

¿Necesito permisos especiales para usar MQTT en Flutter?

En Android, las builds de release necesitan el permiso INTERNET en AndroidManifest.xml (las de debug lo añaden solas). En macOS, activa el entitlement de conexiones salientes (com.apple.security.network.client). iOS y la web no necesitan nada más.