Tutoriales de IoT

Cliente MQTT en Flutter: crea una app IoT multiplataforma

Un tutorial práctico de Flutter MQTT con el paquete mqtt_client para conectar las versiones Android, iOS, escritorio y web a un broker en la nube por TLS o WebSocket seguro, leer datos de sensores en vivo y controlar un dispositivo.

App Flutter MQTT en un teléfono y en un navegador mostrando valores de sensores en vivo y un interruptor de dispositivo conectado a un broker en la nube
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Flutter MQTT permite que una sola base de código Dart hable con tus dispositivos desde Android, iOS, escritorio y la web. En este tutorial conectarás una app Flutter a un broker en la nube con el paquete mqtt_client, mostrarás lecturas de sensores en vivo y enviarás comandos de encendido y apagado a un dispositivo, usando TLS en las plataformas nativas y WebSocket seguro en el navegador.

Qué vas a construir

La app tiene una sola pantalla: un valor de temperatura en vivo que se actualiza cada vez que un dispositivo publica, un chip con el estado de la conexión y un interruptor que publica un comando para encender o apagar un relé. Por debajo usa dos topics:

Topic Dirección Ejemplo de payload
connectmq/demo/<your-id>/sensor/temperature dispositivo a app {"value": 22.4}
connectmq/demo/<your-id>/relay/set app a dispositivo ON o OFF

Para las pruebas usamos el servidor MQTT público gratuito en broker.connectmq.com, pensado para pruebas y prototipos. Acepta conexiones anónimas con cualquier client ID no vacío, pero todo lo que circula por él es legible por cualquiera, así que usa un prefijo de topic aleatorio y nunca envíes datos reales. Cuando pases a producción, sustituye las credenciales por las de tu propio broker de ConnectMQ; la estructura del código no cambia.

Elegir un paquete MQTT para Flutter

La opción más utilizada es mqtt_client en pub.dev. Incluye dos clases de cliente:

  • MqttServerClient para Android, iOS, macOS, Windows y Linux. Habla TCP plano, TLS o WebSocket.
  • MqttBrowserClient para Flutter web, que solo puede usar WebSocket porque los navegadores no permiten sockets TCP sin procesar.

mqtt_client implementa MQTT 3.1.1. Si necesitas funciones de MQTT 5 como la expiración de mensajes o las propiedades de usuario, el mismo autor mantiene mqtt5_client con una API muy parecida. Para una app de panel y comandos como esta, 3.1.1 es perfectamente suficiente. Si estás comparando bibliotecas entre lenguajes, la panorámica de bibliotecas cliente MQTT repasa las ventajas y desventajas.

Añade la dependencia:

flutter pub add mqtt_client

Puertos y transportes

El broker expone cuatro listeners y tú eliges uno por plataforma:

Plataforma Clase Transporte Puerto
Android, iOS, escritorio MqttServerClient TLS 8883
Android, iOS, escritorio (tras firewalls estrictos) MqttServerClient con useWebSocket WSS 8084, ruta /mqtt
Flutter web MqttBrowserClient WSS 8084, ruta /mqtt
Solo depuración local MqttServerClient TCP 1883

El puerto 1883 no está cifrado, así que resérvalo para experimentos rápidos. Todo lo que salga de tu portátil debe usar 8883 u 8084. Si quieres el contexto sobre los transportes en el navegador, lee MQTT sobre WebSocket.

Construir el servicio de conexión MQTT en Flutter

Mantén MQTT fuera de tus widgets. Una pequeña clase de servicio es dueña del cliente, expone un stream de temperaturas y un estado de conexión, y ofrece un método setRelay. Flutter web no puede importar dart:io, así que creamos el cliente mediante una importación condicional.

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, validado contra el almacén de confianza del sistema
  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 (_) {
        // Ignorar payloads mal formados en lugar de romper el 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();
  }
}

Aquí importan algunos detalles. autoReconnect junto con resubscribeOnAutoReconnect hace que una red móvil inestable no te deje conectado pero sordo. startClean() es lo adecuado para un cliente de interfaz que solo se interesa por los datos en vivo; un cliente que no deba perder comandos mientras está desconectado usaría en su lugar una sesión persistente (consulta sesiones persistentes en MQTT). El análisis del JSON va envuelto en un try porque cualquiera puede publicar en un topic público.

La interfaz en 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}'),
          ],
        ),
      ),
    );
  }
}

El prefijo de topic se muestra en pantalla para que puedas copiarlo en una terminal y hacer pruebas. Si estás valorando Flutter frente a stacks nativos o de JavaScript, compáralo con las versiones de la misma app como cliente MQTT para iOS en Swift y con React Native MQTT.

Pruebas con un dispositivo simulado

Todavía no necesitas hardware. Abre dos terminales y haz de dispositivo con las herramientas de línea de comandos de Mosquitto. Sustituye PREFIX por el valor que muestra la app:

# Observar los comandos que envía la app
mosquitto_sub -h broker.connectmq.com -p 1883 -i sim-sub-81723 -t "PREFIX/relay/set" -v

# Publicar una lectura de temperatura falsa cada 2 segundos
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

Acciona el interruptor en la app y deberías ver llegar ON u OFF a la terminal del suscriptor. Si más adelante conectas una placa real, el tutorial de ESP32 con MQTT usa el mismo patrón de topics.

Estructurar los topics MQTT de Flutter para más dispositivos

Un solo dispositivo es fácil. Cuando la app crece hasta una lista de dispositivos, evita crear una suscripción por cada widget de dispositivo. Suscríbete una vez a un comodín como <prefix>/+/sensor/temperature, extrae el ID del dispositivo del topic en _onMessage y emite un mapa indexado por dispositivo en lugar de un único valor. Así el número de suscripciones se mantiene constante, algo importante en brokers compartidos con límites por cliente; el broker público permite 10 suscripciones por cliente.

Otros dos hábitos merecen la pena. Haz que los dispositivos publiquen su estado actual con el flag retain, para que una app recién abierta muestre al instante el estado real del relé en lugar de un false por defecto; la guía de mensajes retenidos en MQTT explica cómo hacerlo. Y deja que el dispositivo confirme los comandos en un topic de estado aparte, como relay/state, en vez de fiarte de la posición del interruptor. La interfaz debe reflejar lo que informa el dispositivo, no lo que pulsó el usuario, de modo que si se pierde un comando el interruptor vuelva a su estado real con el siguiente informe.

Pasar del broker de pruebas a producción

El servidor público es para experimentos. Para un producto real, crea un broker privado en un broker MQTT gestionado para que solo tus dispositivos y apps puedan conectarse:

  1. Regístrate en el portal de ConnectMQ, elige un plan y crea un broker.
  2. Copia el usuario, la contraseña y el client ID generados.
  3. Construye el servicio con ellos: MqttService(username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', clientId: 'YOUR_CLIENT_ID', topicPrefix: 'acme/site1').
  4. Mantén TLS en el puerto 8883 (o WSS en el 8084 para la web).

En ConnectMQ, los client ID están vinculados a las credenciales emitidas, así que cada instalación de la app que se conecte al mismo tiempo necesita su propia identidad. No distribuyas una contraseña compartida dentro de cada copia de una app pública; emite credenciales por usuario o por dispositivo desde un backend que controles, y da por hecho que todo lo que va incrustado en un binario móvil se puede extraer.

Problemas por plataforma y solución de errores

Síntoma Causa probable Solución
La build de release en Android no conecta, la de debug sí Falta el permiso de internet Añade <uses-permission android:name="android.permission.INTERNET"/> al AndroidManifest.xml principal
La app de macOS falla con "Operation not permitted" El sandbox bloquea la red saliente Activa com.apple.security.network.client en los archivos de entitlements
La build web nunca conecta Se usa MqttServerClient o ws:// en una página HTTPS Usa MqttBrowserClient con wss:// en el 8084 y la ruta /mqtt
Conecta y luego se cae cada minuto Keep-alive más largo que el timeout de inactividad del NAT Reduce keepAlivePeriod a entre 20 y 30 segundos
Los mensajes se detienen cuando la app vuelve al primer plano Socket cerrado en segundo plano, sin resuscripción Mantén activados autoReconnect y resubscribeOnAutoReconnect
Conexión rechazada, "identifier rejected" Client ID vacío o duplicado Usa un client ID único y no vacío por sesión

Los sistemas operativos móviles suspenden las apps en segundo plano de forma agresiva. No intentes mantener abierta una conexión MQTT mientras la app está en segundo plano en iOS; vuelve a conectar cuando la app regrese al primer plano y usa notificaciones push para las alertas que deban llegar a un teléfono en reposo. La guía del intervalo keep-alive de MQTT explica cómo ajustar los timeouts para redes móviles.

Si necesitas ver lo que realmente circula por la red, el portal de ConnectMQ incluye un cliente MQTT en el navegador y un explorador de topics en vivo, que es la forma más rápida de confirmar si un problema está en tu app o en el dispositivo.

Empieza con ConnectMQ

Tu app Flutter está lista para un broker real. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, copia las credenciales generadas en MqttService y tus builds de Android, iOS, escritorio y web compartirán el mismo endpoint seguro. Cuando se te quede pequeño el nivel gratuito, el mismo código funciona en los planes de pago y dedicados.

Preguntas frecuentes

¿Funciona Flutter MQTT en Flutter web?

Sí, pero solo a través de WebSocket. Usa MqttBrowserClient con wss://broker.connectmq.com/mqtt en el puerto 8084; los navegadores no pueden abrir conexiones TCP sin procesar a los puertos 1883 u 8883.

¿El paquete mqtt_client es compatible con MQTT 5?

No, mqtt_client implementa MQTT 3.1.1. Usa el paquete hermano mqtt5_client si necesitas funciones de MQTT 5 como las propiedades de usuario o la expiración de mensajes.

¿Puedo mantener viva una conexión MQTT mientras mi app Flutter está en segundo plano?

No de forma fiable en móviles, porque iOS y Android suspenden los sockets en segundo plano. Vuelve a conectar al reanudar y usa notificaciones push para las alertas que deban llegar a los usuarios cuando la app está cerrada.

¿Qué QoS debería usar una app Flutter?

QoS 1 es un buen valor por defecto para comandos y lecturas que no quieres perder. El plan gratuito Serverless de ConnectMQ admite QoS hasta 1, y los planes de pago añaden QoS 2.

Artículos relacionados

Las mejores herramientas CLI de MQTT para pruebas, scripts y depuración

Una comparativa práctica de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI de MQTT.js, curl y mqttui, con la misma prueba ejecutada contra broker.connectmq.com para que elijas la herramienta adecuada para scripts y depuración.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.