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 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 _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 _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 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 _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 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((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 sendCommand(String deviceId, Map 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:
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
client.connectionStatus?.stateNeed a production Flutter IoT app with MQTT? [Contact Code Caracal](/contact) — we've delivered 10+ Flutter IoT apps across iOS and Android.