Tutoriais

MQTT no Flutter com mqtt_client: mobile, desktop e web

Conecte um app Flutter a um broker MQTT com mqtt_client: cliente server vs browser, TLS na 8883, WebSockets seguros no Flutter web, reconexão e testes.

Atualizado · 9 min de leitura

MQTT no Flutter com mqtt_client: mobile, desktop e web

Para usar MQTT no Flutter, adicione o pacote mqtt_client, crie um MqttServerClient para Android, iOS e desktop ou um MqttBrowserClient para Flutter web, conecte, assine e escute o stream updates. O que mais confunde é que os dois clientes usam transportes diferentes: o cliente server fala TCP ou TLS, enquanto o cliente browser só pode usar WebSockets.

Este tutorial monta um pequeno serviço MQTT reutilizável para Flutter, mostra as configurações TCP, TLS e WebSocket seguro contra broker.connectmq.com e termina com uma forma rápida de testar o caminho completo pelo navegador.

Instalar o mqtt_client

O mqtt_client é um pacote Dart puro, então funciona em todas as plataformas Flutter sem plugins nativos:

Adicionar a dependência
flutter pub add mqtt_client

O pacote traz duas classes de cliente em duas bibliotecas separadas. A que você importa define o transporte:

ClasseImportPlataformasTransportes
MqttServerClientpackage:mqtt_client/mqtt_server_client.dartAndroid, iOS, macOS, Windows, LinuxTCP, TLS, WebSocket, WebSocket seguro
MqttBrowserClientpackage:mqtt_client/mqtt_browser_client.dartFlutter webApenas WebSocket e WebSocket seguro

O mqtt_server_client.dart importa dart:io, que não existe no navegador, então importá-lo num build web quebra na compilação. Vamos resolver isso mais adiante com um import condicional. Se você está escrevendo uma ferramenta de linha de comando em Dart puro em vez de um app, a mesma API vale: veja MQTT em Dart.

Primeira conexão via TCP

Comece pela configuração mais simples: TCP sem criptografia na porta 1883. Todo cliente precisa de um client ID único; se dois dispositivos usam o mesmo, o broker derruba a sessão mais antiga sempre que o outro conecta. O gerador de client ID ajuda a escolher um 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; // milissegundos
  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(); // ou client.connect('username', 'password')
  } on Exception catch (e) {
    print('falha ao conectar: $e');
    client.disconnect();
    rethrow;
  }

  final state = client.connectionStatus?.state;
  if (state != MqttConnectionState.connected) {
    throw Exception('o broker recusou a conexão: ${client.connectionStatus}');
  }
  return client;
}

O connect() lança NoConnectionException quando o broker rejeita o CONNECT (credenciais erradas, client ID bloqueado) e uma SocketException quando o host ou a porta não podem ser alcançados. Imprimir client.connectionStatus mostra o código de retorno, que você pode consultar na tabela de reason codes MQTT.

Assinar e receber mensagens

As mensagens recebidas chegam em client.updates, um stream Dart. Assine primeiro e depois escute uma única vez, não uma vez por assinatura:

Assinar e escutar
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');
    }
  });
}

O bytesToStringAsString decodifica os bytes como texto. Para JSON, passe o resultado para jsonDecode; para payloads binários (CBOR, Protobuf) use diretamente publish.payload.message, que é um buffer de bytes tipado.

Num widget, guarde a StreamSubscription retornada por listen e cancele-a em dispose(), ou exponha o stream pela sua solução de gerenciamento de estado (Provider, Riverpod, Bloc) para que os widgets sejam reconstruídos a partir de uma única fonte de verdade, em vez de cada um abrir a própria conexão.

Publicar

Os payloads são montados com MqttClientPayloadBuilder:

Publicar uma leitura 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,
  );
}

Use addUTF8String em vez de addString quando o texto puder ter caracteres não ASCII; o addString grava um byte por unidade de código UTF-16 e pode corromper acentos, cedilhas e emojis.

TLS na porta 8883

Em produção, mude o cliente server para TLS. Brokers com certificado de uma CA pública, como broker.connectmq.com, são verificados pelo repositório de confiança do sistema operacional, então nenhum arquivo extra é necessário:

Configuração TLS
import 'dart:io';

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

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

Para um broker com certificado autoassinado ou CA privada, crie um SecurityContext(withTrustedRoots: false) e chame setTrustedCertificatesBytes() com o certificado da CA carregado dos seus assets. Evite onBadCertificate: (_) => true fora de um experimento local rápido: ele aceita qualquer certificado e anula o TLS sem avisar. Se o handshake falhar, testar a conexão TLS pelo terminal mostra se o problema está no broker ou no app.

Flutter web: MqttBrowserClient via WSS

No navegador, o MQTT roda sobre WebSockets. Páginas servidas por HTTPS só podem abrir conexões wss://, então use a porta WebSocket segura e inclua o path:

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

Repare que a porta é definida como propriedade, não dentro da URL. A linha websocketProtocols envia o header de subprotocolo mqtt, que a maioria dos brokers espera. Um path errado costuma aparecer como um fechamento do WebSocket logo após o upgrade; MQTT sobre WebSockets explica o handshake em detalhes.

Um único código para todas as plataformas

Crie um lib/mqtt_server.dart equivalente que retorne um MqttServerClient (por exemplo na 8883 com TLS) e escolha o arquivo certo em tempo de compilação:

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

O restante do app importa apenas mqtt_factory.dart e package:mqtt_client/mqtt_client.dart, e trabalha com a classe base comum MqttClient. Connect, subscribe, publish e o stream updates se comportam igual nos dois.

Reconexão e ciclo de vida do app

Redes móveis derrubam conexões o tempo todo: troca de Wi-Fi para dados móveis, túneis, o sistema suspendendo seu app. O mqtt_client tem reconexão automática embutida:

Reconexão automática
client
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true
  ..onAutoReconnect = () => print('conexão perdida, reconectando...')
  ..onAutoReconnected = () => print('reconectado');

Com resubscribeOnAutoReconnect (o padrão é true) o cliente refaz suas assinaturas após reconectar, o que importa porque uma clean session as esquece no lado do broker. Numa clean session o broker não guarda mensagens publicadas enquanto você estava desconectado; se o app não pode perder dados, conecte com sessão persistente (omita startClean(), mantenha um client ID estável) e assine com QoS 1. Teste isso na sua própria instância privada de broker: o endpoint compartilhado de testes não guarda sessões offline.

Lembre que iOS e Android suspendem apps em segundo plano, o que fecha o socket independentemente do keep-alive. Observe o ciclo de vida com WidgetsBindingObserver, desconecte de forma limpa em paused e reconecte em resumed. Para entrega em segundo plano, a ferramenta certa são as notificações push; veja desconexões em segundo plano no Android e limites de segundo plano no iOS.

Checklist por plataforma

  • Android: adicione <uses-permission android:name="android.permission.INTERNET"/> em android/app/src/main/AndroidManifest.xml. Builds de debug incluem a permissão automaticamente, por isso "funciona em debug, falha em release" é tão comum.
  • macOS: adicione com.apple.security.network.client com valor true em DebugProfile.entitlements e Release.entitlements, ou o sandbox bloqueia sockets de saída.
  • iOS: nada extra para TLS. O App Transport Security vale para as APIs de carregamento de URL, não para TCP direto nem ws:// para um hostname, mas use TLS em produção mesmo assim.
  • Web: use MqttBrowserClient, wss://, a porta e o path corretos. O navegador bloqueia conteúdo misto (um broker ws:// a partir de uma página https://).

Testar o caminho completo

Rode o app, abra o cliente MQTT online num navegador, conecte em broker.connectmq.com via WebSocket seguro e assine testmqtt/flutter-demo/#. Você deve ver as leituras que o app publica, e o que você publicar do navegador em testmqtt/flutter-demo/sensors/test deve aparecer no console de debug do Flutter.

Veja no navegador as mensagens do seu app Flutter

Se nada chegar, verifique nesta ordem: o estado da conexão depois do connect(), se o tópico da assinatura casa com o da publicação (o topic matcher ajuda com wildcards) e a saída de debug após client.logging(on: true). As exceções mais frequentes e suas correções estão reunidas em erros MQTT no Flutter.

Com um arquivo factory, um único stream de mensagens e reconexão automática ativada, o mesmo código Flutter conversa com o broker a partir de um celular, um desktop e uma aba do navegador. Comece pelo endpoint compartilhado, passe para TLS e WebSockets seguros com credenciais, e mantenha o cliente no navegador aberto durante o desenvolvimento para ver cada mensagem que o app envia.

Perguntas frequentes

Por que meu app Flutter com MQTT funciona no Android mas não na web?

Navegadores não conseguem abrir sockets TCP diretos, então o MqttServerClient não funciona no Flutter web. Use o MqttBrowserClient com uma URL wss://, a porta WebSocket do broker (por exemplo 8084) e o path correto, como /mqtt.

O mqtt_client suporta MQTT 5?

Não. O mqtt_client implementa MQTT 3.1 e 3.1.1. Se você precisa de recursos do MQTT 5 como reason codes, user properties ou session expiry, use o pacote mqtt5_client do mesmo autor, que tem uma API bem parecida.

Preciso de permissões especiais para usar MQTT no Flutter?

No Android, builds de release precisam da permissão INTERNET no AndroidManifest.xml (builds de debug adicionam sozinhas). No macOS, ative o entitlement de conexões de saída (com.apple.security.network.client). iOS e web não precisam de nada extra.