Back to Blog
Mobile Development

Flutter + MQTT: Building Production IoT Mobile Apps That Scale

Flutter's mqtt_client library can feel finicky to get right. This guide covers TLS authentication, topic management, Riverpod state integration, battery-friendly keep-alives, and the reconnection patterns that keep IoT apps reliable in the field.

August 20, 2024
11 min read
FlutterMQTTIoTmqtt_client

Flutter + MQTT: Building Production IoT Mobile Apps That Scale

MQTT is the dominant IoT protocol — it's why AWS IoT Core, Google Cloud IoT, and Azure IoT Hub all support it natively. When your Flutter app needs to receive real-time device telemetry and send commands, MQTT is the right choice.

But getting Flutter MQTT right in production requires more than following the mqtt_client README. This guide covers TLS auth, state management integration, reconnection logic, and the battery optimization tricks that matter in real-world deployments.

Package Setup

dependencies:
  mqtt_client: ^10.2.0
  riverpod: ^2.5.0
  flutter_riverpod: ^2.5.0

Step 1: Secure MQTT Connection with TLS

Never connect to MQTT without TLS in production. If your broker uses client certificate authentication (mTLS):

import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

class MQTTService { late MqttServerClient _client;

static const String _broker = 'your-broker.iot.region.amazonaws.com'; static const int _port = 8883; static const String _clientId = 'flutter-app-${Platform.operatingSystem}';

Future<void> connect() async { _client = MqttServerClient.withPort(_broker, _clientId, _port); _client.secure = true; _client.keepAlivePeriod = 60; // seconds — balance between battery and connection detection _client.connectTimeoutPeriod = 10000; // ms _client.autoReconnect = false; // We handle reconnect ourselves _client.logging(on: false);

// Load certs from assets (or secure storage in production) final context = SecurityContext.defaultContext; context.useCertificateChainBytes(await _loadAsset('certs/client.crt')); context.usePrivateKeyBytes(await _loadAsset('certs/client.key')); context.setTrustedCertificatesBytes(await _loadAsset('certs/ca.crt')); _client.securityContext = context;

final connMessage = MqttConnectMessage() .withClientIdentifier(_clientId) .withWillQos(MqttQos.atLeastOnce) .startClean(); _client.connectionMessage = connMessage;

_client.onDisconnected = _onDisconnected; _client.onConnected = _onConnected; _client.onSubscribed = _onSubscribed;

try { await _client.connect(); } on NoConnectionException catch (e) { _scheduleReconnect(); } }

Future<Uint8List> _loadAsset(String path) async { final data = await rootBundle.load(path); return data.buffer.asUint8List(); } }

For username/password auth (simpler, still needs TLS):

await _client.connect(username, password);

Step 2: Reconnection with Exponential Backoff

Network conditions on mobile are brutal. Your MQTT connection will drop on tunnel entry, subway stations, and airplane mode. Handle it gracefully:

int _reconnectAttempts = 0;
bool _intentionalDisconnect = false;

void _onDisconnected() { if (!_intentionalDisconnect) { _scheduleReconnect(); } }

Future<void> _scheduleReconnect() async { final delays = [2, 5, 10, 30, 60, 120]; final delay = delays[_reconnectAttempts.clamp(0, delays.length - 1)]; _reconnectAttempts++;

await Future.delayed(Duration(seconds: delay));

try { await connect(); // Resubscribe to all topics after reconnect for (final topic in _activeSubscriptions) { _subscribe(topic); } _reconnectAttempts = 0; } catch (_) { _scheduleReconnect(); } }

Future<void> disconnect() async { _intentionalDisconnect = true; _client.disconnect(); }

Step 3: Topic Management

Define your topic structure as constants — never hardcode topic strings in widgets:

class MQTTTopics {
  static String telemetry(String deviceId) => 'devices/$deviceId/telemetry';
  static String commands(String deviceId)  => 'devices/$deviceId/commands';
  static String status(String deviceId)    => 'devices/$deviceId/status';
  static const String allTelemetry         = 'devices/+/telemetry';
}

// In MQTTService final Set<String> _activeSubscriptions = {};

void subscribeToDevice(String deviceId) { final topic = MQTTTopics.telemetry(deviceId); _subscribe(topic); }

void _subscribe(String topic) { _client.subscribe(topic, MqttQos.atLeastOnce); _activeSubscriptions.add(topic); }

// Parse incoming messages Stream<MQTTMessage> get messageStream { return _client.updates!.expand((messages) { return messages.map((msg) { final topic = msg.topic; final payload = MqttPublishPayload.bytesToStringAsString( (msg.payload as MqttPublishMessage).payload.message, ); return MQTTMessage(topic: topic, payload: payload); }); }); }

Step 4: Riverpod Integration

// providers.dart
final mqttServiceProvider = Provider((ref) {
  final service = MQTTService();
  ref.onDispose(service.disconnect);
  return service;
});

// Automatically connect on app start final mqttConnectionProvider = FutureProvider((ref) async { final service = ref.watch(mqttServiceProvider); await service.connect(); return service; });

// Device telemetry stream for a specific device final deviceTelemetryProvider = StreamProvider.family<DeviceData, String>((ref, deviceId) { final service = ref.watch(mqttServiceProvider); service.subscribeToDevice(deviceId);

return service.messageStream .where((msg) => msg.topic == MQTTTopics.telemetry(deviceId)) .map((msg) => DeviceData.fromJson(jsonDecode(msg.payload))); });

// In your widget
class DeviceCard extends ConsumerWidget {
  final String deviceId;
  const DeviceCard(this.deviceId);

@override Widget build(BuildContext context, WidgetRef ref) { final data = ref.watch(deviceTelemetryProvider(deviceId));

return data.when( data: (d) => Column(children: [ Text('${d.temperature}°C'), Text(d.isOnline ? 'Online' : 'Offline'), ]), loading: () => const LinearProgressIndicator(), error: (_, __) => const Icon(Icons.wifi_off, color: Colors.red), ); } }

Step 5: Publishing Commands

Future<void> sendCommand(String deviceId, Map<String, dynamic> command) async {
  final topic   = MQTTTopics.commands(deviceId);
  final payload = jsonEncode(command);

final builder = MqttClientPayloadBuilder(); builder.addString(payload);

_client.publishMessage( topic, MqttQos.atLeastOnce, // At least once delivery builder.payload!, retain: false, ); }

// Usage await mqttService.sendCommand('device-001', { 'action': 'set_relay', 'relay': 1, 'state': true, });

Battery Optimization

MQTT's keep-alive ping prevents the connection from timing out but drains battery:

  • 1. Set keep-alive to 60s — not lower. 30s is unnecessary on mobile.
  • 2. Disconnect in background after 5 minutes on Android/iOS and reconnect on foreground
  • 3. Use QoS 0 for telemetry (no ACK round-trip) when data loss is acceptable
  • 4. Use QoS 1 for commands (at-least-once guaranteed delivery)
  • 5. Topic wildcards judiciously — devices/+/telemetry is fine; # is not (receives everything)
  • // Detect app lifecycle changes
    class AppLifecycleObserver extends WidgetsBindingObserver {
      final MQTTService mqttService;

    @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { // Background: disconnect after delay Future.delayed(const Duration(minutes: 5), mqttService.disconnect); } else if (state == AppLifecycleState.resumed) { mqttService.connect(); } } }

    Common Pitfalls

  • Duplicate clientId: Two devices with the same clientId will kick each other off the broker
  • Subscribing before confirming connected: Check client.connectionStatus?.state
  • Not resubscribing after reconnect: Track active subscriptions and replay on reconnect
  • Blocking the main thread: All MQTT operations are async — don't await in build()
  • Need a production Flutter IoT app with MQTT? [Contact Code Caracal](/contact) — we've delivered 10+ Flutter IoT apps across iOS and Android.

    ⚡
    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options