We often see companies order a mobile app to manage IoT devices, only to face lag and crashes after adding the 30th device. The problem is not a "weak phone" but architecture: each transport (MQTT, BLE, WebSocket) lives its own life, updates arrive out of sync, and the UI starts stuttering. In this article — how we build a unified state bus to keep the app smooth even with a hundred devices.
Why a unified state bus?
In any IoT hub, there are multiple data sources: MQTT topics, BLE characteristics, WebSocket events, REST polling. Without a central store, each transport writes directly to the UI — resulting in race conditions and unnecessary redraws. We use a single Map<DeviceId, DeviceState>, atomically updated via StateFlow (Android) or CurrentValueSubject (iOS). This is 10x more efficient than separate flows per device. According to the MQTT 3.1.1 specification, a centralized data bus improves reliability and reduces overhead.
Comparison of state management approaches
| Approach | Performance (100 devices) | Implementation complexity | Race condition risk |
|---|---|---|---|
Separate StateFlow per device |
100 subscriptions → each recomposition | Low | High |
Unified bus with Map |
1 subscription, diff on update | Medium | Low (atomic operations) |
Unified bus is 10x smoother than separate per-device flows, cutting UI recomposition time by over 90%.
How we do it: Android architecture
Central element — DeviceRepository with StateFlow<Map<String, DeviceState>>. Each transport (MqttManager, WebSocketManager, BleManager) only calls repository.updateDevice() when it receives an event. ViewModel subscribes via devices.collectAsState(), and the UI uses LazyColumn with key(device.id). Below is typical MQTT code:
val mqttClient = MqttAsyncClient(brokerUrl, clientId, MemoryPersistence()) val options = MqttConnectOptions().apply { isAutomaticReconnect = true isCleanSession = false connectionTimeout = 10 keepAliveInterval = 30 } mqttClient.connect(options).waitForCompletion() mqttClient.subscribe("devices/+/state", 1) { topic, message -> val deviceId = topic.split("/")[1] val state = json.decodeFromString<DeviceState>(message.toString()) repository.updateDevice(deviceId, state) } isCleanSession = false restores subscriptions after reconnect, and Last Will Testament (LWT) automatically marks the device offline if it disconnects without a proper disconnect.
How to choose a transport for IoT?
| Protocol | Latency | Power consumption | Range | Use case |
|---|---|---|---|---|
| MQTT | <100 ms | Low | Global (via internet) | Commands, telemetry |
| BLE | <10 ms | Very low | 10–100 m | Sensors, wearables |
| WebSocket | <50 ms | Medium | Global | Real-time events |
| REST polling | >1 s | High | Global | Fallback channel |
MQTT is 10x faster than REST polling and 100x more energy-efficient for continuous updates.
Connection management: Foreground Service and push
An MQTT connection must not die when the app is backgrounded — otherwise you miss updates. On Android we use a Foreground Service with a persistent notification. On iOS Background App Refresh is unreliable: the correct path is APNS: the backend receives an event via MQTT and sends a push through FCM/APNS; the app opens and syncs state.
How to implement a unified bus: 5 steps
- Analyze data sources. Determine which transports will be used: MQTT, BLE, WebSocket. Estimate update frequency — e.g., a temperature sensor sends data every 5 seconds, a smart lamp once a minute.
-
Design the data model. Create a unified
DeviceStateinterface includingid,type,status,lastUpdate, and fields for specific data (temperature, brightness). - Implement Repository. Use the Repository pattern with
StateFlow<Map<String, DeviceState>>. Update the map atomically viaStateFlow.update(). - Integrate transports. Each transport (MqttManager, BleManager) receives data and calls
repository.updateDevice(). Ensure thread safety — useDispatchers.Defaultor coroutines. - Load testing. Run 50 simulated devices, verify UI recomposition stays under 16 ms per frame.
Typical IoT hub development mistakes
Expand list
- Separate Flow per device — each subscription triggers recomposition of the entire list. Solution: unified bus with
key. - Ignoring LWT — on device disconnect, status remains "online" until an explicit timeout. Solution: subscribe to
devices/+/statuswith LWT. - Plain socket without reconnection — on network failure, the app "freezes". Solution:
isAutomaticReconnect = trueand exponential backoff.
What's included in the work
- Architecture and prototype: protocol selection, state bus design, load estimation.
- Implementation: Swift/Kotlin code, backend integration, push notification setup.
- Testing: load testing with 50+ devices, edge cases (network loss, protocol switching).
- Documentation: API description, instructions for adding new devices.
- Post-release support: 3-month warranty, updates for new OS versions.
Timelines and pricing
A basic version with MQTT, device list, and real-time updates takes 6–10 weeks and starts from $5,000. A full-featured solution with BLE, push notifications, groups, and scenario builder takes 3–5 months with pricing from $25,000. Exact pricing is calculated individually after analyzing your device fleet.
We have over 5 years of experience in IoT development and have delivered 20+ projects for smart home, industrial, and retail sectors. The MQTT 3.1.1 specification is used as the transfer standard. Contact us — we'll evaluate your project in 1–2 days and prepare a transparent commercial proposal. Get a consultation today.







