Разработка мобильного IoT-хаба для управления устройствами
IoT-хаб в мобильном приложении — это не просто список устройств с кнопками включения. Это менеджер подключений по нескольким протоколам, реалтайм-обновление состояния сотен устройств одновременно и корректная обработка ситуаций, когда половина устройств офлайн. Неправильная архитектура даёт тормозящий интерфейс и непредсказуемые краши при масштабировании выше 20–30 девайсов.
Архитектура: единая шина состояния устройств
Центральная проблема — разнородные источники данных. Устройства приходят через MQTT, WebSocket, Bluetooth, REST polling. Всё это нужно свести в единый Map<DeviceId, DeviceState>, который реактивно обновляет UI.
На Android — StateFlow в DeviceRepository на уровне Application:
class DeviceRepository {
private val _devices = MutableStateFlow<Map<String, DeviceState>>(emptyMap())
val devices: StateFlow<Map<String, DeviceState>> = _devices.asStateFlow()
fun updateDevice(id: String, state: DeviceState) {
_devices.update { current -> current + (id to state) }
}
}
Каждый транспорт (MqttManager, WebSocketManager, BleManager) вызывает repository.updateDevice() при получении события. ViewModel подписывается на devices.collectAsState() в Compose. Нет гонок — update {} атомарен.
Не делать отдельный StateFlow на каждое устройство — при 100 девайсах это 100 подписок в UI, и recomposition в Compose будет срабатывать по очереди на каждое обновление, что визуально выглядит как мерцание списка.
MQTT: главный транспорт для IoT
Paho MQTT Client для Android — стандартный выбор. Версия 1.2.5+, подключение через MqttAndroidClient или корутинный wrapper. QoS 1 для команд (at least once), QoS 0 для телеметрии (fire and forget — потеря одного значения датчика некритична).
val mqttClient = MqttAsyncClient(brokerUrl, clientId, MemoryPersistence())
val options = MqttConnectOptions().apply {
isAutomaticReconnect = true
isCleanSession = false
connectionTimeout = 10
keepAliveInterval = 30
userName = username
password = password.toCharArray()
}
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 — подписки восстанавливаются после реконнекта. Без этого после потери сети нужно заново подписываться на все топики вручную.
Last Will Testament (LWT) — механизм MQTT для детектирования отключения устройства. При подключении девайса к брокеру он указывает LWT-сообщение; если девайс отвалился без явного disconnect, брокер публикует LWT. Мобильное приложение подписывается на devices/+/status и обновляет isOnline = false при получении LWT.
Управление подключениями: lifecycle и фоновая работа
MQTT-соединение не должно умирать при сворачивании приложения — иначе пуши об изменении состояния устройств не придут. foreground Service с уведомлением — правильный паттерн для Android. WorkManager для этой задачи не подходит: он не гарантирует постоянную работу.
На iOS: Background App Refresh ненадёжен для постоянного соединения. Правильный путь — APNS: устройство публикует статус через MQTT → бэкенд получает → отправляет push через FCM/APNS. Мобильное приложение узнаёт об изменении статуса через push, а не через живой WebSocket.
UI: список устройств с реалтаймом
Список устройств с обновляющимися данными — LazyColumn в Compose или RecyclerView с DiffUtil. Ключевой момент: обновление одного устройства не должно перерисовывать весь список. В Compose — key(device.id) в LazyColumn:
LazyColumn {
items(devices, key = { it.id }) { device ->
DeviceCard(device = device)
}
}
DeviceCard рендерится только при изменении конкретного device объекта. Без key — каждое обновление любого устройства вызывает recomposition всего списка.
Группировка и фильтрация
В реальных установках 50+ устройств — нужны группы (комнаты, зоны, типы) и поиск. Фильтрация — на уровне ViewModel:
val filteredDevices = combine(devices, searchQuery, selectedGroup) { all, query, group ->
all.values.filter { device ->
(group == null || device.group == group) &&
(query.isEmpty() || device.name.contains(query, ignoreCase = true))
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
combine + stateIn — реактивная фильтрация без ручного пересчёта. SharingStarted.WhileSubscribed(5000) — останавливает поток через 5 секунд после последнего subscriber, экономит ресурсы при сворачивании.
Сроки
Мобильный IoT-хаб с MQTT, WebSocket, списком устройств и реалтайм-обновлением: 6–10 недель. Мультипротокольное решение с BLE, MQTT, REST polling, push-уведомлениями и конструктором групп: 3–5 месяцев. Стоимость рассчитывается индивидуально.







