Flutter MQTT lets one Dart codebase talk to your devices from Android, iOS, desktop and the web. In this tutorial you will connect a Flutter app to a cloud broker with the mqtt_client package, show live sensor readings and send on/off commands to a device, using TLS on native platforms and secure WebSocket in the browser.
What You Will Build
The app has one screen: a live temperature value that updates whenever a device publishes, a connection status chip, and a switch that publishes a command to turn a relay on or off. Under the hood it uses two topics:
| Topic | Direction | Payload example |
|---|---|---|
connectmq/demo/<your-id>/sensor/temperature |
device to app | {"value": 22.4} |
connectmq/demo/<your-id>/relay/set |
app to device | ON or OFF |
For testing we use the public MQTT test broker at broker.connectmq.com. It accepts anonymous connections with any non-empty client ID, but everything on it is readable by anyone, so use a random topic prefix and never send real data. When you move to production you swap in the credentials of your own ConnectMQ broker; the code does not change shape.
Choosing a Flutter MQTT Package
The most widely used option is mqtt_client on pub.dev. It ships two client classes:
MqttServerClientfor Android, iOS, macOS, Windows and Linux. It speaks plain TCP, TLS, or WebSocket.MqttBrowserClientfor Flutter web, which can only use WebSocket because browsers do not allow raw TCP sockets.
mqtt_client implements MQTT 3.1.1. If you need MQTT 5 features such as message expiry or user properties, the same author maintains mqtt5_client with a very similar API. For a dashboard-and-commands app like this one, 3.1.1 is perfectly adequate. If you are comparing libraries across languages, the overview of MQTT client libraries covers the trade-offs.
Add the dependency:
flutter pub add mqtt_client
Ports and Transports
The broker exposes four listeners, and you pick one per platform:
| Platform | Class | Transport | Port |
|---|---|---|---|
| Android, iOS, desktop | MqttServerClient |
TLS | 8883 |
| Android, iOS, desktop (behind strict firewalls) | MqttServerClient with useWebSocket |
WSS | 8084, path /mqtt |
| Flutter web | MqttBrowserClient |
WSS | 8084, path /mqtt |
| Local debugging only | MqttServerClient |
TCP | 1883 |
Port 1883 is unencrypted, so keep it for quick experiments. Anything that leaves your laptop should use 8883 or 8084. If you want the background on browser transports, read MQTT over WebSocket.
Building the Flutter MQTT Connection Service
Keep MQTT out of your widgets. A small service class owns the client, exposes a stream of temperatures and a connection state, and offers a setRelay method. Flutter web cannot import dart:io, so we create the client through a conditional import.
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, validated against the system trust store
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 (_) {
// Ignore malformed payloads instead of crashing the 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();
}
}
A few details matter here. autoReconnect plus resubscribeOnAutoReconnect means a flaky mobile network does not leave you connected but deaf. startClean() is right for a UI client that only cares about live data; a client that must not miss commands while offline would use a persistent session instead (see MQTT persistent sessions). The JSON parse is wrapped because anyone can publish to a public topic.
The Flutter UI
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}'),
],
),
),
);
}
}
The topic prefix is printed on screen so you can copy it into a terminal for testing. If you are weighing Flutter against native or JavaScript stacks, compare this with the iOS MQTT client in Swift and React Native MQTT versions of the same app.
Testing With a Simulated Device
You do not need hardware yet. Open two terminals and act as the device with the Mosquitto command-line tools. Replace PREFIX with the value shown in the app:
# Watch the commands the app sends
mosquitto_sub -h broker.connectmq.com -p 1883 -i sim-sub-81723 -t "PREFIX/relay/set" -v
# Publish a fake temperature reading every 2 seconds
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
Flip the switch in the app and you should see ON or OFF arrive in the subscriber terminal. If you later wire up a real board, the ESP32 MQTT tutorial uses the same topic pattern.
Structuring Flutter MQTT Topics for More Devices
One device is easy. When the app grows to a list of devices, avoid creating one subscription per device widget. Subscribe once to a wildcard such as <prefix>/+/sensor/temperature, parse the device ID out of the topic in _onMessage, and emit a map keyed by device instead of a single value. That keeps the subscription count constant, which matters on shared brokers with per-client limits; the public broker allows 10 subscriptions per client.
Two more habits pay off. Have devices publish their current state with the retain flag, so a freshly opened app shows the real relay state immediately instead of a default false; the guide to MQTT retained messages explains how. And let the device confirm commands on a separate state topic such as relay/state rather than trusting the switch position. The UI should reflect what the device reports, not what the user tapped, so if a command is lost the switch snaps back to the true state on the next report.
Moving From the Test Broker to Production
The public broker is for experiments. For a real product, create a private broker on a fully managed MQTT service so only your devices and apps can connect:
- Sign up at the ConnectMQ portal, choose a plan and create a broker.
- Copy the generated username, password and client ID.
- Construct the service with them:
MqttService(username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', clientId: 'YOUR_CLIENT_ID', topicPrefix: 'acme/site1'). - Keep TLS on port 8883 (or WSS on 8084 for web).
On ConnectMQ, client IDs are bound to issued credentials, so each app installation that connects at the same time needs its own identity. Do not ship one shared password inside every copy of a public app; issue credentials per user or per device from a backend you control, and treat anything embedded in a mobile binary as extractable.
Platform Pitfalls and Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Android release build cannot connect, debug works | Missing internet permission | Add <uses-permission android:name="android.permission.INTERNET"/> to the main AndroidManifest.xml |
| macOS app fails with "Operation not permitted" | Sandbox blocks outgoing network | Enable com.apple.security.network.client in the entitlements files |
| Web build never connects | Using MqttServerClient or ws:// on an HTTPS page |
Use MqttBrowserClient with wss:// on 8084 and path /mqtt |
| Connects, then drops every minute | Keep-alive longer than an idle NAT timeout | Lower keepAlivePeriod to 20 to 30 seconds |
| Messages stop after the app resumes | Socket killed in background, no resubscribe | Keep autoReconnect and resubscribeOnAutoReconnect on |
| Connection refused, "identifier rejected" | Empty or duplicate client ID | Use a unique, non-empty client ID per session |
Mobile operating systems suspend background apps aggressively. Do not try to hold an MQTT connection open while the app is in the background on iOS; reconnect when the app returns to the foreground and use push notifications for alerts that must reach a sleeping phone. The MQTT keep-alive interval guide explains how to tune timeouts for cellular networks.
If you need to see what is actually on the wire, the ConnectMQ portal includes an in-browser MQTT client and a live topic explorer, which is the quickest way to confirm whether a problem is in your app or in the device.
Get Started with ConnectMQ
Your Flutter app is ready for a real broker. Create a free Serverless broker at app.connectmq.com, no credit card required, copy the generated credentials into MqttService, and your Android, iOS, desktop and web builds will all share the same secure endpoint. When you outgrow the free tier, the same code works on paid and dedicated plans.
Frequently Asked Questions
Does Flutter MQTT work on Flutter web?
Yes, but only over WebSocket. Use MqttBrowserClient with wss://broker.connectmq.com/mqtt on port 8084; browsers cannot open raw TCP connections to port 1883 or 8883.
Does the mqtt_client package support MQTT 5?
No, mqtt_client implements MQTT 3.1.1. Use the sibling package mqtt5_client if you need MQTT 5 features such as user properties or message expiry.
Can I keep an MQTT connection alive while my Flutter app is in the background?
Not reliably on mobile, because iOS and Android suspend background sockets. Reconnect on resume and use push notifications for alerts that must reach users when the app is closed.
Which QoS should a Flutter app use?
QoS 1 is a good default for commands and readings you do not want to lose. The free ConnectMQ Serverless plan supports QoS up to 1, and paid plans add QoS 2.



