This page is the practical one for an infotainment engineer: the patterns that work when your data comes from services you do not own, on a vehicle whose configuration you cannot assume.
Pattern 1 — Discover, never assume#
class ComfortViewModel(private val vehicle: VehicleServices) {
init {
// Discover once, at startup, and shape the UI from what is there.
val seat = vehicle.discover<SeatService>(minVersion = 1)
val massage = seat?.supports("massage") == true
_uiState.value = ComfortUi(
showSeatControls = seat != null,
showMassage = massage,
showAmbientLight = vehicle.discover<LightingService>() != null,
)
}
}Pattern 2 — Render from confirmation, never from the request#
This is the rule that appears throughout this site, and it matters most in IVI because IVI is where the driver looks.
fun onTemperatureSliderChanged(target: Float) {
// Send the request. Do NOT update the displayed value.
climate.setTargetTemperature(target)
showPending()
}
// The displayed value only ever comes from here.
private val onTemperatureChanged = { actual: Float ->
_displayed.value = actual
clearPending()
}Pattern 3 — Tolerate version skew in both directions#
IVI ships monthly; the service layer may ship yearly. Both directions of mismatch will occur in the field.
val seat = vehicle.discover<SeatService>(minVersion = 1)
// A capability added in 1.3 — the vehicle in front of you may be on 1.1.
if (seat != null && seat.version >= Version(1, 3)) {
enableMemoryPresets()
} else {
hideMemoryPresets()
}Pattern 4 — Subscribe at the rate the pixel needs#
// A gauge redrawing at 60 fps does not need 60 Hz of data.
vehicle.subscribe(SPEED, rate = Rate.UI) // ~5 Hz
.collect { speed -> gauge.setTarget(speed) } // interpolate between samplesPattern 5 — Handle the service disappearing#
Services restart. Processors reboot. Links drop.
vehicle.onServiceLost<ClimateService> {
_uiState.update { it.copy(climateAvailable = false) } // grey it out
}
vehicle.onServiceFound<ClimateService> { service ->
_uiState.update { it.copy(climateAvailable = true) }
resubscribe(service)
}Pattern 6 — Do not process on the callback thread#
override fun onValueChanged(signal: String, value: Any) {
// This is a transport thread. Do nothing expensive here.
pending[signal] = value
scheduleRender() // coalesce and render on your own thread
}Testing all of this without a vehicle#
The patterns above are exactly what a data broker makes testable.
# The value your UI should display
kuksa-client set Vehicle.Cabin.HVAC.AmbientAirTemperature 22.5
# Simulate a refusal — target set, current never follows
kuksa-client set-target Vehicle.Cabin.HVAC.TargetTemperature 19
# ... and deliberately do not update the current value
# Simulate the service disappearing
docker stop climate-serviceNext#
Getting applications onto the vehicle in the first place.

