IoT Telemetry Dashboard in Mobile Applications
IoT dashboard — a screen showing data from multiple sensors simultaneously: current values, trends, historical charts, device statuses. Key problem — performance with many widgets and frequently updated data. Poor architecture causes UI lag, skipped updates, and battery drain.
Data Architecture for Dashboard
Dashboard aggregates data from different sources: MQTT topics with realtime telemetry, REST API for historical data, WebSocket for events. All must be unified into a single ViewModel that reactively updates widgets.
On Android — combine multiple StateFlow:
class DashboardViewModel : ViewModel() {
private val temperatureFlow = mqttRepository.getTopicFlow("sensors/+/temperature")
private val humidityFlow = mqttRepository.getTopicFlow("sensors/+/humidity")
private val devicesFlow = deviceRepository.devices
val dashboardState = combine(
temperatureFlow,
humidityFlow,
devicesFlow
) { temperatures, humidities, devices ->
DashboardState(
sensors = devices.map { device ->
SensorWidgetData(
id = device.id,
name = device.name,
temperature = temperatures[device.id],
humidity = humidities[device.id],
isOnline = device.isOnline
)
}
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DashboardState())
}
SharingStarted.WhileSubscribed(5_000) — stream stops 5 seconds after screen goes background. Saves connection, data reloads on return.
Widgets: What and How to Render
Typical IoT dashboard contains:
- Numeric widgets — current value + unit + status (normal/warning/critical)
- Mini-line charts — trend over past hour
- Gauge/Speedometer — for parameters with clear bounds (pressure, CO2 level)
- Device status cards — online/offline + latest data
On Android Compose: each widget is separate @Composable with device key. LazyVerticalGrid for grid:
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(dashboardState.sensors, key = { it.id }) { sensor ->
SensorWidget(
sensor = sensor,
modifier = Modifier.animateItemPlacement()
)
}
}
animateItemPlacement() — smooth animation when adding/removing widgets. Without key — Compose completely redraws list on each update.
Numeric widgets and frequent updates. MQTT may send data every second. Updating UI that often drains battery. Throttle at Flow level:
temperatureFlow
.throttleLatest(1000) // no more than once per second
.collect { updateWidget(it) }
throttleLatest instead of debounce — shows latest value over period, doesn't wait for pause.
Historical Data: Loading and Caching
On screen open, need historical data for past N hours for mini-charts. Loading all data at once when opening dashboard is bad if many devices.
Pattern: load history lazily as widgets appear in viewport. LazyColumn/LazyGrid + LaunchedEffect on first widget display:
@Composable
fun SensorWidget(sensor: SensorWidgetData, viewModel: DashboardViewModel) {
LaunchedEffect(sensor.id) {
viewModel.loadHistory(sensor.id, hours = 1)
}
// Show skeleton while data loads
val history by viewModel.getHistoryFlow(sensor.id).collectAsState(emptyList())
MiniChart(data = history)
}
Cache history in Room with TTL: data older than 5 minutes — fetch from server again. Fresh — return from cache without network.
Dashboard Customization by User
Drag widgets, add/remove sensors — optional but valuable feature. On Android: ItemTouchHelper for RecyclerView or ReorderableItem from compose-reorderable library. Save widget order in SharedPreferences or DataStore.
Implementing dashboard with realtime updates, historical charts, and adaptive grid: 4–6 weeks. Pricing calculated individually.







