IoT Tutorials

Flutter MQTT Client: Building a Cross-Platform IoT App

A hands-on Flutter MQTT tutorial using the mqtt_client package to connect Android, iOS, desktop and web builds to a cloud broker over TLS or secure WebSocket, read live sensor data and control a device.

Flutter MQTT app on a phone and in a browser showing live sensor values and a device toggle connected to a cloud broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

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:

  • MqttServerClient for Android, iOS, macOS, Windows and Linux. It speaks plain TCP, TLS, or WebSocket.
  • MqttBrowserClient for 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:

  1. Sign up at the ConnectMQ portal, choose a plan and create a broker.
  2. Copy the generated username, password and client ID.
  3. Construct the service with them: MqttService(username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', clientId: 'YOUR_CLIENT_ID', topicPrefix: 'acme/site1').
  4. 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.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.