My Android app has a speedometer that tells how fast the user is driving. However, it is not working smoothly. It tells the speed but not the exact speed and does not update the speed smoothly.
Am I doing something wrong here? Is there any other way to fetch user speed?
class SpeedometerVM(application: Application) : AndroidViewModel(application) {
private val _speed = MutableStateFlow(0f) // Speed in km/h
val speed = _speed.asStateFlow()
private val _isActive = MutableStateFlow(false)
val isActive = _isActive.asStateFlow()
private val _accelerationStatus =
MutableStateFlow("Neutral") // Acceleration/Deceleration status
val accelerationStatus = _accelerationStatus.asStateFlow()
// Variable to store previous speed with timestamp for more accurate acceleration calculation
private var previousSpeed = 0f
private var lastUpdateTime = 0L
private val fusedLocationClient by lazy {
LocationServices.getFusedLocationProviderClient(getApplication<Application>())
}
// Keep a reference to the callback so we can remove it later
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
val currentTimestamp = System.currentTimeMillis()
val speedInMetersPerSecond = location.speed
// Only update if we have a valid speed reading
if (location.hasSpeed()) {
val speedInKmPerHour = speedInMetersPerSecond * 3.6f // Convert m/s to km/h
// Apply a small smoothing filter to reduce jitter
val smoothedSpeed = if (_speed.value == 0f) {
speedInKmPerHour
} else {
_speed.value * 0.7f + speedInKmPerHour * 0.3f
}
_speed.value = smoothedSpeed
// If enough time has passed, calculate acceleration status
if (lastUpdateTime > 0 && (currentTimestamp - lastUpdateTime) > 500) {
val speedDiff = smoothedSpeed - previousSpeed
// Only update status if change exceeds threshold
val newStatus = when {
speedDiff > 0.5 -> "Accelerating"
speedDiff < -0.5 -> "Decelerating"
else -> "Neutral"
}
if (newStatus != _accelerationStatus.value) {
_accelerationStatus.value = newStatus
}
}
// Update the previous values
previousSpeed = smoothedSpeed
lastUpdateTime = currentTimestamp
}
}
}
}
fun startLocationUpdates() = registerLocationUpdates()
private fun registerLocationUpdates() {
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000L)
.setMinUpdateIntervalMillis(500) // Set minimum update time
.setMaxUpdateDelayMillis(2000) // Set maximum delay
.build()
viewModelScope.launch {
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
_isActive.value = true
} catch (e: SecurityException) {
_isActive.value = false
// Handle permission exception
logger("SpeedometerViewModel", "Location permission error: ${e.message}")
}
}
}
/**
* Stops location updates
*/
fun stopLocationUpdates() {
viewModelScope.launch {
fusedLocationClient.removeLocationUpdates(locationCallback)
_isActive.value = false
_speed.value = 0.0f
_accelerationStatus.value = "Stable"
previousSpeed = 0.0f
}
}
override fun onCleared() {
super.onCleared()
if (_isActive.value) {
stopLocationUpdates()
}
}
}
Collecting this stateflow using
val speed by speedVM.speed.collectAsStateWithLifecycle()
You using : collectAsStateWithLifecycle() is correct
Just make sure that :
you’re inside a @Composable function
you’re using the lifecycle-runtime-compose dependency
your SpeedometerVM is correctly scoped to the screen
If your speed still jumps too much. You can apply a low-pass filter like this , instead of just 70–30 blend:
val alpha = 0.25f
val smoothedSpeed = _speed.value + alpha * (newSpeed - _speed.value)
This will give smoother transitions and avoids jittery behavior. Hope it helps.