Skip to content

Conversation

@jgillich
Copy link
Collaborator

Description

Adds a LocationProvider that enhances another LocationProvider with the bearing values of the device rotation sensor. It provides accurate bearing values when the fused location provider is unable to do so (commonly due to lack of movement).

iOS is still TODO

cc @kodebach in case you have some thoughts as the author of the location package

Test plan

Provider is enabled in demo app for testing

Have you tested the changes? On which platforms?

  • Android: 16

Adds a LocationProvider that enhances another LocationProvider with the bearing values of the device rotation sensor. It provides accurate bearing values when the default location provider is unable to do so (commonly due to lack of movement).
Copy link
Contributor

@kodebach kodebach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bearing from LocationManager and the heading/orientation from SensorManager are to different things. The bearing is based on direction of travel, while orientation is based on compass heading.

I chose to use bearing partly because it is available through LocationManager, but also because it seems more relevant to our use case. If we want to support using compass heading, the logic should probably be integrated into the normal LocationProvider with an option to switch between bearing and heading. On iOS both are provided by CLLocationManager and for the GMS module there is FusedOrientationProviderClient.

I'll leave the decision on how to handle this to @sargunv

@jgillich
Copy link
Collaborator Author

The bearing from LocationManager and the heading/orientation from SensorManager are to different things. The bearing is based on direction of travel, while orientation is based on compass heading.

Indeed, they are different things, but you don't get useful readings from location while standing still and you don't get useful readings from compass while moving. This combines them in a way that makes sense for most applications - but perhaps not all. We could add this a separate heading value on Location, I might've been overly focused on my own use case 😄

@sargunv
Copy link
Collaborator

sargunv commented Nov 12, 2025

The bearing from LocationManager and the heading/orientation from SensorManager are to different things.

I haven't looked into the code yet, but I'd love to understand (1) what's different, and (2) in what use cases one might prefer one over the other.

My assumptions, may be wrong: one is the direction I'm traveling, the other is the direction the phone is facing. If that's correct, I definitely see use cases for both.

  1. When connected to a vehicle, the direction the vehicle is facing
  2. When navigating, the direction I'm traveling
  3. Otherwise, the direction I'm facing

Very curious how other maps combine these

@jgillich jgillich marked this pull request as draft November 12, 2025 21:24
@jgillich
Copy link
Collaborator Author

one is the direction I'm traveling, the other is the direction the phone is facing

That is my understanding too. Navigation apps will generally assume that your phone is facing in the direction you're traveling, so they're merging these in similar ways (Ride with GPS and Cyclers are two examples that do this). But some apps may want to implement their own logic for handling them, so the current approach isn't flexible enough

How about a HeadingProvider that can be passed to LocationProvider to compute a dedicated heading on Location?

@jgillich jgillich mentioned this pull request Nov 12, 2025
5 tasks
@kodebach
Copy link
Contributor

As I explain in #714 (comment), I think we need to support both heading and bearing and make it configurable. There are definitely use cases for both. The native MapLibre Android SDK also allows switching the RenderMode and CameraMode between COMPASS and GPS.

How about a HeadingProvider that can be passed to LocationProvider to compute a dedicated heading on Location?

Yes, I agree Location should have additional heading and headingAccuracy (in degrees since both iOS and GMS provide accuracy in degrees) fields. Not sure, however, about the separate HeadingProvider. I don't really see a use for it. While heading needs a separate listener on all platforms AFAICT, I can't see a good use case for combining different HeadingProviders and LocationProviders.

I would suggest:

public interface LocationProvider {
  public val headingEnabled: Boolean
  public val location: StateFlow<Location?>

  public fun enableHeading(enabled: Boolean)
}

By default the heading is disabled. Calling enableHeading(true) starts the heading listener, enableHeading(false) stops it again. That way we can listen to heading only, when needed.

On the UserLocationState we need something like:

public class UserLocationState internal constructor(
  locationState: State<Location?>,
  headingEnabledState: MutableState<Boolean>,
) {
  public val location: Location? by locationState
  public var headingEnabled: Boolean? by headingEnabledState
}


@Composable
public fun rememberUserLocationState(
  locationProvider: LocationProvider,
  headingEnabled: Boolean = false,
  lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
  minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
  coroutineContext: CoroutineContext = EmptyCoroutineContext,
): UserLocationState {
  val headingEnabledState = mutableStateOf(headingEnabled)
  val locationState =
    locationProvider.location.collectAsStateWithLifecycle(
      lifecycleOwner = lifecycleOwner,
      minActiveState = minActiveState,
      context = coroutineContext,
    )
  LaunchedEffect(headingEnabledState.value) {
    locationProvider.enableHeading(headingEnabledState.value)
  }
  return remember(locationState) { UserLocationState(locationState, enableHeadingState) }
}

(Note: It feels a little odd to have the LaunchedEffect and all the logic in the remember function and not the state class itself. Maybe there is a better way to do this)

The LocationPuck would also need a way of switching between heading and bearing for the indicator.

@jgillich
Copy link
Collaborator Author

Yea I agree, a pluggable HeadingProvider is probably not needed.

public fun enableHeading(enabled: Boolean)

Is there a use case where you'd need to toggle this frequently? I would've just keyed this so compose makes a new provider on change

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

As far as terminology, I'd avoid calling one "bearing" and the other "heading"; it's not clear at all what it means to "enable heading".

A bearing is a direction on the surface of the world. That bearing could the the direction the device is moving, or it could be the direction the device is facing.

I'd recommend designing the API around three discrete concepts:

  • position (latitude/longitude)
  • motion (direction, speed)
  • orientation or attitude (direction)

This could translate to something like:

// using types from spatial-k

class PositionMeasurement {
  val position: Position
  val inaccuracy: Length?
}

class BearingMeasurement {
  val bearing: Bearing
  val inaccuracy: Rotation?
}

class SpeedMeasurement {
  val distancePerSecond: Length
  val inaccuracy: Length?
}

class DeviceLocationState {
  val position: PositionMeasurement?
  val movementDirection: BearingMeasurement?
  val movementSpeed: SpeedMeasurement?
  val facingDirection: BearingMeasurement?
}

And then the LocationPuck accepts any PositionMeasurement and BearingMeasurement:

val l = rememberDeviceLocationState(
  // ...
  requestPosition = true,
  requestMovementDirection = true,
  requestMovementSpeed = true,
  requestFacingDirection = true,
)

// ^ or perhaps even split those up into their own remember functions and state flows
// all consuming the same provider

LocationPuck(
  position = l.position,
  bearing = l.movementDirection // or l.facingDirection
)

And then it's up to the user if/how they want to fuse facing and movement direction. It might be based on the mode in the app (navigating vs browsing), or relative accuracy, or any other app-specific needs

fun minByInaccuracy(a: BearingMeasurement, b: BearingMeasurement) =
  if (a.inaccuracy <= b.inaccuracy) a else b

LocationPuck(
  position = l.position,
  bearing = minByInaccuracy(l.movementDirection, l.facingDirection)
)

@jgillich
Copy link
Collaborator Author

That looks good, but I think we can shorten the property names a bit. Movement direction on iOS is aptly named course

class DeviceLocationState {
  val position: PositionMeasurement?
  val course: BearingMeasurement?
  val speed: SpeedMeasurement?
  val facing: BearingMeasurement?
}

Also, is DeviceLocationState meant to replace UserLocationState? I actually wonder what the purpose of UserLocationState is, since it contains just a single field

@kodebach
Copy link
Contributor

Also, is DeviceLocationState meant to replace UserLocationState?

IIUC DeviceLocationState is what's currently called Location. I would definitely not call it anything *State because that implies something quite specific in compose. IMO it should also stay a data class or implement equals, hashCode and toString manually, if we prefer that. Comparing Location is not only a sensible action, it's actually necessary for StateFlow and collectAsStateWithLifecycle to discard unchanged values properly.

I actually wonder what the purpose of UserLocationState is, since it contains just a single field

rememberUserLocationState could return State<Location> yes, but then you'd need to call it as

val location by rememberUserLocationState(...)

or use it like this

val locationState = rememberUserLocationState(...)

SomeComposable(location = locationState.value)

with the UserLoationState class we get

val locationState = rememberUserLocationState(...)

SomeComposable(location = locationState.location)

and crucially we abstract away the implementation detail of State<Location>, which makes it much easier if we need to add other properties, as suggested above for headingEnabled. (Note: headingEnabled is still necessary if we want to toggle the additional listener for compass heading, but let's maybe call it compassEnabled).

The only thing I don't like about the current UserLocationState is all the initialization logic lives outside the class.

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

Ah yeah, my mistake with the state naming

I'd call the data class something like LocationUpdate perhaps?

It may also make sense to separate location providers (position, course, speed) from orientation providers, as the underlying platform API is totally separate on all the platforms I checked (iOS/macOS, Android, Web, Windows). Though maybe it's better for us to combine them into one abstraction? Not sure here

@jgillich
Copy link
Collaborator Author

jgillich commented Nov 13, 2025

Did a first draft of this, will be away for the weekend so I'll finish it next week.

  • Location is still Location
  • Movement direction is course and facing is orientation
  • Tracking features are configured on LocationRequest, although only the orientation setting is honored at this time. I'm not sure if being able to turn off position tracking is useful in a maps library, I think I'd rather have Location.position as non-nullable. Also, LocationRequest does have a name clash with GMS, maybe we should use another name
  • Converted a few values to spatialk units. We could do this in a few more places, but I'm only touching location stuff here
  • The SensorManager-based provider doesn't set orientation accuracy since we don't get it in degrees. Most people should be using GMS anyway

@jgillich
Copy link
Collaborator Author

It may also make sense to separate location providers (position, course, speed) from orientation providers, as the underlying platform API is totally separate on all the platforms I checked

FusedLocationProviderClient and FusedOrientationProviderClient are in the same package on Android so not totally separate. On iOS they are both on CLLocationManager as far as I can tell

But sure, we could separate them if we wanted to, I don't really have an opinion on that tbh 😄

@jgillich jgillich changed the title Sensor-enhanced location provider Device orientation support Nov 13, 2025
@kodebach
Copy link
Contributor

It may also make sense to separate location providers (position, course, speed) from orientation providers

I think that's an unnecessary complication. Apart from Android (and maybe desktop if we ever support it) all platforms will probably only ever have a single implementation for location and orientation providers. Even on Android with the GMS special case, I'd don't think there would be big use case in combining platform location with GMS orientation or vice versa. If someone really needs it, they can always implement their own LocationProvider since the interface is public.

I think separating them just introduces unnecessary combining logic, unless we completely separate the two and only combine them at the UserLocationState level like this:

public class UserLocationState internal constructor(
  locationState: State<Location?>,
  orientationState: State<Orientation?>,
) {
  public val location: Location? by locationState
  public var orientation: Orientation? by orientationState
}


@Composable
public fun rememberUserLocationState(
  locationProvider: LocationProvider,
  orientationProvider: OrientationProvider? = null,
  headingEnabled: Boolean = false,
  lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
  minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
  coroutineContext: CoroutineContext = EmptyCoroutineContext,
): UserLocationState {
  val orientationState =
    orientationProvider?.orientation?.collectAsStateWithLifecycle(
      lifecycleOwner = lifecycleOwner,
      minActiveState = minActiveState,
      context = coroutineContext,
    )
  val locationState =
    locationProvider.location.collectAsStateWithLifecycle(
      lifecycleOwner = lifecycleOwner,
      minActiveState = minActiveState,
      context = coroutineContext,
    )
  return remember(locationState, orientationState) {
    UserLocationState(locationState, orientationState ?: mutableStateOf(null))
  }
}

That has pros and cons. On the pro side, it makes it clear that these are separate things that might update at different rates. On the con side, it also means that LocationPuck and user code may need to combine/synchronise the updates themselves.

Copy link
Contributor

@kodebach kodebach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the nullable position in Location. A location without a position doesn't make sense IMO. Same goes for toggling speed and course separately from position in LocationRequest. AFAIK these come from the same API on all platforms, i.e. we get all of them for the same cost. No reason to control them separately.

The API otherwise LGTM except for a few minor details. What I forgot to mention before is, that UserLocationState also exists to ensure we can pass around a remembered value that doesn't change with ever location/orientation update, which let's us avoid recomposition.

* instead of e.g. [kotlin.time.Instant], to allow calculating how old a location is, even if the
* system clock changes.
*/
@Serializable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess making it @Serializable is neat, but is the serialization plugin even applied? AFAIK if it's not applied to the module containing the class @Serializable is useless

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be, it's in library-conventions

@Serializable public data class BearingMeasurement(val bearing: Bearing, val inaccuracy: Rotation?)

@Serializable
public data class SpeedMeasurement(val distancePerSecond: Length, val inaccuracy: Length?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does spatial-k not have a speed unit? That should probably be added upstream.

Then we could also use a single generic Measurement class

public data class Measurement<T, A>(val value: T, val inaccuracy: A?)

which would turn the slightly silly locationState.location.position.position and locationState.location.speed.distancePerSecond into simpler locationState.location.position.value and locationState.location.speed.value.

IMO this generic Measurement could also be a case for upstreaming, because it could be extended with functionality for doing calculations with measurements while correctly propagating the inaccuracies.

id = "$idPrefix-bearing",
source = locationSource,
visible = showBearing && locationState.location?.bearing != null,
visible = bearing != null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to put the whole SymbolLayer behind an if. There is no reason to add an always invisible layer, if bearing is disabled.

@Composable
public fun LocationTrackingEffect(
locationState: UserLocationState,
location: Location?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work for two reasons:

  1. I'm pretty sure it would mean the LocationTrackingEffect recomposes every time the location changes
  2. The snapshotFlow would never emit a new value, even if it wasn't constantly recomposed, because its lambda doesn't contain any state reads.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I was going to revisit that file later, just had Claude do a quick fix to make it build

public fun LocationPuck(
idPrefix: String,
locationState: UserLocationState,
location: Location?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
location: Location?,
position: PositionMeasurement?,

Using Location might be slightly nicer, but (1) it hides the decision between course and orientation and (2) means LocationPuck will recompose if only the location.speed changed, even though we're not using it. (With the state class this wasn't the case because UserLocationState is always remembered)

Comment on lines +110 to 117
/** update camera orientation based on location course (direction of movement) */
TRACK_COURSE,

/** update camera orientation based on device orientation (heading) */
TRACK_ORIENTATION,

/** update camera orientation based on course if available, otherwise orientation */
TRACK_LOCATION,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** update camera orientation based on location course (direction of movement) */
TRACK_COURSE,
/** update camera orientation based on device orientation (heading) */
TRACK_ORIENTATION,
/** update camera orientation based on course if available, otherwise orientation */
TRACK_LOCATION,
/** update camera rotation based on location course (direction of movement) */
TRACK_COURSE,
/** update camera rotation based on device orientation (heading) */
TRACK_ORIENTATION,
/** update camera rotation based on course if available, otherwise orientation */
TRACK_AUTOMATIC,

if (!request.orientation) {
return@let flow
}
flow.zip(orientation) { location, (orientation, orientationAccuracy) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zip is not the right operator here. If we never get a orientation, we should still emit a location without it, so we should use combine(orientation.sample(1.seconds)) to slow down orientation to the level of the location flow, but still emit if orientation never emits or is slower.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine also doesn't emit until all flows have emitted at least once. Is it even possible to not get any orientation values? Other than faulty hardware?

Guess it doesn't matter if we split orientation into its own provider

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but you can force the orientation flow to emit at least once, by calling send(null) similar to how the location flow calls send(lastLocation). But yeah, doesn't matter, if we split it

if (!request.orientation) {
return@let flow
}
flow.zip(orientation) { location, facing ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in GMS version

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

Apart from Android (and maybe desktop if we ever support it)

Devices without GPS receivers may want to use some network geolocation provider, and this would have no relevance to orientation

Desktop will probably have three platform providers (macOS Core Location, Windows.Devices.Geolocation, Geoclue), maybe encapsulated into one universal "platform" provider for convenience.

it makes it clear that these are separate things that might update at different rates

This would be the strongest justification to separate them I think. I don't have a solid opinion on this yet, will have to play with it a bit.

I'll also be out this weekend, so can take a look next week

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

I don't like the nullable position in Location.

Is it ever possible to receive speed information (say, from an accelerometer) without a position?

If location isn't available (missing hardware, missing permission, no gps signal), what should we return?

If we combine the two providers, then it should be possible to request orientation without location/movement.

A location without a position doesn't make sense IMO.

Agreed, but in the naming sense, which is why I'd name it LocationUpdate (or even DeviceProprioceptionUpdate if we really want to be precise and combine them)

@kodebach
Copy link
Contributor

Devices without GPS receivers may want to use some network geolocation provider.
Desktop will probably have three platform providers
This would be the strongest justification to separate them I think
If location isn't available (missing hardware, missing permission, no gps signal), what should we return?

After looking at the draft and considering the cases above, I'm convinced a separate OrientationProvider would make sense with the UserLocationState from #712 (comment).

It solves quite a few edge cases. There's got to be a reason after all, why GMS also provides separate location and orientation.

With OrientationProvider we should definitely have an Orientation type. Not sure, whether that's a typealias for BearingMeasurement or it's own data class.

Is it ever possible to receive speed information (say, from an accelerometer) without a position?

Technically I guess you could try to estimate speed from an accelerometer, but it would get quite inaccurate quickly. On Android Auto (compose isn't really supported their, but still) and possibly some frankenstein JVM thing you could get technically also get speed data from an actual speedometer.

That said I don't think for the purposes of this library that's relevant. In the end the goal is to support displaying of user locations on a map. That use case doesn't make sense without a position.

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

That said I don't think for the purposes of this library that's relevant. In the end the goal is to support displaying of user locations on a map. That use case doesn't make sense without a position.

That's what we're doing with the LocationPuck, but I'd expect the location update provider to be more agnostic to exactly how the data is used. Maybe an app is showing some speed overlay next to the map.

And even if there's nothing that can be done without a position, it's possible the position isn't available due to permissions, hardware, or the environment. The app needs to be able to handle these cases, and making the property nullable is a way to communicate that.

@kodebach
Copy link
Contributor

That's what we're doing with the LocationPuck, but I'd expect the location update provider to be more agnostic to exactly how the data is used.

I get what you want to achieve here, but I'm not sure it's the right approach. Again location (even "location update") without position makes no sense. If you really see a use case for a provider that provides speed and/or travel direction without position data, then I would suggest:

public data class Location(val position: Position, val inaccuracy: Length?)
public data class Speed(val distancePerSecond: Length, val direction: Rotation?, val directionInaccuracy: Rotation?)
public data class Orientation(val heading: Bearing, val inaccuracy: Rotation?)

public interface LocationProvider {
  public val location: StateFlow<Location?>
}

public interface SpeedProvider {
  public val speed: StateFlow<Speed?>
}

public interface OrientationProvider {
  public val orientation: StateFlow<Orientation?>
}

public class UserLocationState(
  locationState: State<Location?>,
  speedState: State<Speed?>,
  orientationState: State<Orientation?>,
) {
  public val location: Location? by locationState
  public val speed: Speed? by speedState
  public val orientation: Orientation? by orientationState
}

It then becomes possible to implement a pure SpeedProvider if somebody needs it. The default implementations would still link location and speed, because the platform provides them together, but orientation is separate where a separate listener is needed. UserLocationState still bundles it all up for compose.

On Android:

public class AndroidLocationProvider :  LocationProvider, SpeedProvider
public class AndroidOrientationProvider :  OrientationProvider

public class FusedLocationProvider :  LocationProvider, SpeedProvider
public class FusedOrientationProvider :  OrientationProvider

On iOS:

public class IosLocationProvider : LocationProvider, SpeedProvider, OrientationProvider

@sargunv
Copy link
Collaborator

sargunv commented Nov 13, 2025

I'm not talking about a provider that never provides position. I wouldn't design a model around that.

The question was about nullable position, and so I'm talking about a provider that sometimes can't provide a position (like if permission isn't available, or hardware, or gps signal)

@kodebach
Copy link
Contributor

The question was about nullable position, and so I'm talking about a provider that sometimes can't provide a position
if permission isn't available, or hardware, or gps signal

IMO in the end we should keep in mind where LocationProvider lives. It is part of maplibre-compose a library designed to draw maps. It's main purpose should be therefore be providing position data, since you cannot display speed (without a position) on a map. You can display it next to or on top of the map view, but not as part of the map itself. Anything that is part of the map needs to be anchored to some coordinates, i.e. a position.

With that in mind, I think we can agree that a LocationProvider which doesn't at least once provide a position is useless. At least it seems so based on your comment.

The question then becomes: What happens, if the LocationProvider receives speed and course data faster than position data? IMO the correct solution to this is, to do exactly what FusedLocationProviderClient from GMS does. You update the last known position based on speed and course and return it with an correspondingly worse accuracy value.

Your solution with LocationUpdate and a nullable position would essentially be a way to send the raw data downstream to the app. However, IMO that is not the use case of LocationProvider. Again, it is part of maplibre-compose, not some generic multiplatform geo-related sensor library. Even then, if you want to provide raw sensor data, a separate SpeedProvider is the superior solution, because it shows that these may use different sources with different update rates.

In essence, my main argument boils down to the usability of the API we provide. Making the position nullable, means apps need to constantly deal with a technically possible null position everywhere and for no reason at all, since the LocationProviders we provide all guarantee that the position will never be null. That is just horrible API design. It goes totally against KISS and falls squarely into YAGNI. At least until we actually write a LocationProvider implementation that receives speed data separately from position data.

@sargunv
Copy link
Collaborator

sargunv commented Nov 14, 2025

What non-nullable value do you suggest we provide if we no longer have a GPS signal, or if we no longer have location permissions, or if the location services on the device were disabled, or if we no longer have the hardware to sense location?

The point is, we can't guarantee a position result, the same as an HTTP client can't guarantee a 2xx response. Any app requesting a position needs to be handling the case where a position isn't available, in a manner suitable to that app.

Maybe that's ceasing to display a position, and notifying the user position is not available. Maybe that's falling back to a different provider. Or maybe that's interpolating with last known position and course. It depends on the use case of the app.

@kodebach
Copy link
Contributor

Okay, maybe we misunderstood each other. I'm not suggesting that a LocationProvider can never return a null value to indicate missing data, I'm just talking about the position field within the Location class.

Let's clarify some assumptions, to make sure we're thinking about the same issue:

  1. Do we plan to implement a LocationProvider that directly uses a non-location platform API (*) determine the speed and direction of travel?
    My current assumption is: No, we don't have any plans to do this.
  2. Do you know of any platform location APIs that continue to provide new data on speed or travel direction, when position data is not or no longer available?
    I currently do not know of any such APIs. AFAIK this could only happen, with if you use completely separate platform APIs for speed/direction and location.

(*) platform APIs includes GMS, Geoclue and other third party libraries that we use as the basis of our LocationProviders.

Under my own assumptions, I must conclude that it is not worth it to significantly worsen the experience of using LocationProvider, by using Location(val position: Position?, ...) instead of Location(val position: Position, ...). My reason here is

  1. I do not see a case where one of our LocationProvider implementations would need a Location(position = null, ...) instead of just null
  2. If we do want to integrate speed from a separate source, we should do so via a separate SpeedProvider interface and Speed data class.

@sargunv
Copy link
Collaborator

sargunv commented Nov 15, 2025

Ah sorry, you're correct.

For clarity, the states:

  • location data is available. We have a timestamp, position and optionally altitude, bearing, speed, and their accuracies.
  • location data is unavailable.

And separately, orientation data:

  • orientation data is available. We have a timestamp, bearing and optionally an accuracy.
  • orientation data is not available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants