O MQTT com Flutter permite que uma única base de código Dart converse com os seus dispositivos a partir de Android, iOS, desktop e web. Neste tutorial você vai conectar um app Flutter a um broker na nuvem com o pacote mqtt_client, exibir leituras de sensores ao vivo e enviar comandos de liga/desliga a um dispositivo, usando TLS nas plataformas nativas e WebSocket seguro no navegador.
O que você vai construir
O app tem uma única tela: um valor de temperatura ao vivo que se atualiza sempre que um dispositivo publica, um chip de status da conexão e um switch que publica um comando para ligar ou desligar um relé, por exemplo o da bomba de irrigação de uma fazenda. Por baixo, ele usa dois topics:
| Topic | Direção | Exemplo de payload |
|---|---|---|
connectmq/demo/<your-id>/sensor/temperature |
do dispositivo para o app | {"value": 22.4} |
connectmq/demo/<your-id>/relay/set |
do app para o dispositivo | ON ou OFF |
Para os testes usamos o servidor MQTT público (gratuito) em broker.connectmq.com. Ele aceita conexões anônimas com qualquer client ID não vazio, mas tudo nele pode ser lido por qualquer pessoa, então use um prefixo de topic aleatório e nunca envie dados reais. Quando for para produção, basta trocar pelas credenciais do seu próprio broker ConnectMQ; a estrutura do código não muda.
Escolhendo um pacote MQTT para Flutter
A opção mais usada é o mqtt_client, no pub.dev. Ele traz duas classes de cliente:
MqttServerClientpara Android, iOS, macOS, Windows e Linux. Fala TCP puro, TLS ou WebSocket.MqttBrowserClientpara Flutter web, que só pode usar WebSocket porque os navegadores não permitem sockets TCP brutos.
O mqtt_client implementa o MQTT 3.1.1. Se você precisar de recursos do MQTT 5, como message expiry ou user properties, o mesmo autor mantém o mqtt5_client, com uma API muito parecida. Para um app de dashboard e comandos como este, o 3.1.1 é perfeitamente adequado. Se estiver comparando bibliotecas entre linguagens, a visão geral de bibliotecas cliente MQTT aborda os prós e contras.
Adicione a dependência:
flutter pub add mqtt_client
Portas e transportes
O broker expõe quatro listeners, e você escolhe um por plataforma:
| Plataforma | Classe | Transporte | Porta |
|---|---|---|---|
| Android, iOS, desktop | MqttServerClient |
TLS | 8883 |
| Android, iOS, desktop (atrás de firewalls restritivos) | MqttServerClient com useWebSocket |
WSS | 8084, caminho /mqtt |
| Flutter web | MqttBrowserClient |
WSS | 8084, caminho /mqtt |
| Apenas depuração local | MqttServerClient |
TCP | 1883 |
A porta 1883 não é criptografada, então reserve-a para experimentos rápidos. Tudo o que sair do seu notebook deve usar 8883 ou 8084. Para entender melhor os transportes no navegador, leia MQTT sobre WebSocket.
Construindo o serviço de conexão MQTT no Flutter
Mantenha o MQTT fora dos seus widgets. Uma pequena classe de serviço é dona do cliente, expõe um stream de temperaturas e um estado de conexão e oferece um método setRelay. O Flutter web não pode importar dart:io, então criamos o cliente por meio de um import 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 o repositório de certificados do 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 (_) {
// Ignora payloads malformados em vez de derrubar o 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();
}
}
Alguns detalhes importam aqui. autoReconnect mais resubscribeOnAutoReconnect garante que uma rede móvel instável não deixe você conectado, porém surdo. startClean() é o certo para um cliente de UI que só se interessa por dados ao vivo; um cliente que não pode perder comandos enquanto está offline usaria uma sessão persistente (veja sessões persistentes MQTT). O parse do JSON fica protegido porque qualquer pessoa pode publicar em um topic público.
A UI do 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}'),
],
),
),
);
}
}
O prefixo de topic aparece na tela para que você possa copiá-lo em um terminal durante os testes. Se estiver comparando o Flutter com stacks nativas ou JavaScript, compare com as versões do mesmo app feitas com cliente MQTT para iOS em Swift e MQTT com React Native.
Testando com um dispositivo simulado
Você ainda não precisa de hardware. Abra dois terminais e faça o papel do dispositivo com as ferramentas de linha de comando do Mosquitto. Substitua PREFIX pelo valor mostrado no app:
# Acompanhe os comandos que o app envia
mosquitto_sub -h broker.connectmq.com -p 1883 -i sim-sub-81723 -t "PREFIX/relay/set" -v
# Publique uma leitura de temperatura falsa a 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
Acione o switch no app e você deverá ver ON ou OFF chegar no terminal do assinante. Se depois você ligar uma placa real, o tutorial de MQTT com ESP32 usa o mesmo padrão de topics.
Estruturando topics MQTT no Flutter para mais dispositivos
Um dispositivo é fácil. Quando o app cresce para uma lista de dispositivos, evite criar uma assinatura por widget de dispositivo. Assine uma única vez um curinga como <prefix>/+/sensor/temperature, extraia o ID do dispositivo do topic em _onMessage e emita um mapa indexado por dispositivo em vez de um único valor. Isso mantém constante o número de assinaturas, o que importa em brokers compartilhados com limites por cliente; o servidor MQTT público permite 10 assinaturas por cliente.
Mais dois hábitos compensam. Faça os dispositivos publicarem seu estado atual com a flag retain, para que um app recém-aberto mostre imediatamente o estado real do relé em vez de um false padrão; o guia sobre retained messages no MQTT explica como. E deixe o dispositivo confirmar os comandos em um topic de estado separado, como relay/state, em vez de confiar na posição do switch. A UI deve refletir o que o dispositivo informa, não o que o usuário tocou; assim, se um comando se perder, o switch volta ao estado real no próximo relatório.
Do servidor MQTT público para a produção
O servidor MQTT público serve apenas para testes e experimentos. Para um produto real, crie um broker privado em um broker MQTT gerenciado, para que apenas os seus dispositivos e apps possam se conectar:
- Cadastre-se no portal do ConnectMQ, escolha um plano e crie um broker.
- Copie o usuário, a senha e o client ID gerados.
- Construa o serviço com eles:
MqttService(username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', clientId: 'YOUR_CLIENT_ID', topicPrefix: 'acme/site1'). - Mantenha TLS na porta 8883 (ou WSS na 8084 para a web).
No ConnectMQ, os client IDs ficam vinculados às credenciais emitidas, então cada instalação do app que se conecta ao mesmo tempo precisa da sua própria identidade. Não distribua uma senha compartilhada dentro de cada cópia de um app público; emita credenciais por usuário ou por dispositivo a partir de um backend sob o seu controle, e considere extraível qualquer coisa embutida em um binário mobile.
Armadilhas de plataforma e solução de problemas
| Sintoma | Causa provável | Correção |
|---|---|---|
| O build de release no Android não conecta, o de debug funciona | Falta a permissão de internet | Adicione <uses-permission android:name="android.permission.INTERNET"/> ao AndroidManifest.xml principal |
| O app macOS falha com "Operation not permitted" | O sandbox bloqueia a rede de saída | Ative com.apple.security.network.client nos arquivos de entitlements |
| O build web nunca conecta | Uso de MqttServerClient ou de ws:// em uma página HTTPS |
Use MqttBrowserClient com wss:// na 8084 e caminho /mqtt |
| Conecta e cai a cada minuto | Keep-alive maior que o timeout de inatividade do NAT | Reduza keepAlivePeriod para 20 a 30 segundos |
| As mensagens param depois que o app volta | Socket encerrado em segundo plano, sem nova assinatura | Mantenha autoReconnect e resubscribeOnAutoReconnect ativados |
| Conexão recusada, "identifier rejected" | Client ID vazio ou duplicado | Use um client ID único e não vazio por sessão |
Os sistemas operacionais móveis suspendem apps em segundo plano de forma agressiva. Não tente manter uma conexão MQTT aberta enquanto o app está em segundo plano no iOS; reconecte quando o app voltar ao primeiro plano e use push notifications para alertas que precisam chegar a um celular em repouso. O guia do intervalo de keep-alive MQTT explica como ajustar os timeouts para redes celulares.
Se você precisar ver o que realmente está trafegando, o portal do ConnectMQ inclui um cliente MQTT no navegador e um explorador de topics ao vivo, a forma mais rápida de confirmar se o problema está no seu app ou no dispositivo.
Comece a usar o ConnectMQ
O seu app Flutter está pronto para um broker real. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, copie as credenciais geradas para o MqttService, e os seus builds Android, iOS, desktop e web passarão a compartilhar o mesmo endpoint seguro. Quando o plano gratuito ficar pequeno, o mesmo código funciona nos planos pagos e dedicados.
Perguntas frequentes
O MQTT com Flutter funciona no Flutter web?
Sim, mas apenas via WebSocket. Use MqttBrowserClient com wss://broker.connectmq.com/mqtt na porta 8084; os navegadores não conseguem abrir conexões TCP brutas nas portas 1883 ou 8883.
O pacote mqtt_client suporta MQTT 5?
Não, o mqtt_client implementa o MQTT 3.1.1. Use o pacote irmão mqtt5_client se precisar de recursos do MQTT 5, como user properties ou message expiry.
Posso manter uma conexão MQTT ativa enquanto o meu app Flutter está em segundo plano?
Não de forma confiável no mobile, porque o iOS e o Android suspendem sockets em segundo plano. Reconecte ao retomar o app e use push notifications para alertas que precisam chegar aos usuários quando o app está fechado.
Qual QoS um app Flutter deve usar?
QoS 1 é um bom padrão para comandos e leituras que você não quer perder. O plano gratuito ConnectMQ Serverless suporta QoS até 1, e os planos pagos acrescentam o QoS 2.



