Location
Find out where the user is, which way they face and whether they are walking, turn an address into
coordinates and back, and keep tracking in the background or watch for entering a region.
@symbiote-native/location wraps
expo-location so every
SymbioteNative adapter can reach it, not just React. Background updates and geofencing register as
tasks through @symbiote-native/task-manager.
Functions and streams live in a shared core. The three permission hooks
(useForegroundPermissions, useBackgroundPermissions, useMotionActivityPermissions) are
per-adapter, and Angular gets one *PermissionsService each.
| OS platform | Support |
|---|---|
| iOS | live |
| Android | live |
| Framework adapter | Support |
|---|---|
| React | live |
| Vue | live |
| Angular | live |
| Svelte | live |
| Solid | live |
Installation
Section titled “Installation”npm install @symbiote-native/locationScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --location (or
add --location in an existing app) installs and wires this for you, and asks whether to grant
background location - see
@symbiote-native/cli.
expo-location and expo-modules-core come along as regular dependencies, pinned to exact
versions. Never install either yourself, and never add the expo meta-package to your project (it
bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
What the package adds on install (nothing overwrites a value you set):
| Platform | Added | Why |
|---|---|---|
| iOS | NSLocationWhenInUseUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription, NSLocationAlwaysUsageDescription, NSMotionUsageDescription |
Permission prompt texts. Reword them to fit your app |
| iOS | UIBackgroundModes: location |
Without it iOS stops delivering updates once the app backgrounds |
| Android | ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION |
Ship in expo-location’s own manifest and merge automatically |
Background location is opt-in
Section titled “Background location is opt-in”Android’s background location permissions are a deliberate app choice: requesting
ACCESS_BACKGROUND_LOCATION triggers Google Play policy review. The new --location and
add --location commands ask whether to grant them. If you said no, or ran non-interactively, run
it any time (it is safe to repeat):
npx @symbiote-native/cli grant locationThat adds ACCESS_BACKGROUND_LOCATION, FOREGROUND_SERVICE and FOREGROUND_SERVICE_LOCATION to
your AndroidManifest.xml. ACTIVITY_RECOGNITION (needed for motion activity) is still added by
hand. Pass the foregroundService option to startLocationUpdatesAsync so Android can keep
tracking while the app is backgrounded.
Ask for permission, then read the position once or watch it:
import { getCurrentPositionAsync, requestForegroundPermissionsAsync, watchPositionAsync,} from '@symbiote-native/location';
const { granted } = await requestForegroundPermissionsAsync();if (granted) { const position = await getCurrentPositionAsync(); const subscription = await watchPositionAsync({ distanceInterval: 10 }, location => { console.log(location.coords); }); // later: subscription.remove();}Show permission state
Section titled “Show permission state”To render the permission state, use the adapter’s own binding. Each is shown for the foreground permission; the background and motion-activity hooks have the same shape.
import { useForegroundPermissions } from '@symbiote-native/location/react';
export default function LocationGate() { const [status, requestPermission] = useForegroundPermissions();
if (!status?.granted) { return <button title="Allow location" onPress={() => requestPermission()} />; } return <text>Location allowed</text>;}<script setup lang="ts">import { useForegroundPermissions } from '@symbiote-native/location/vue';
const [status, requestPermission] = useForegroundPermissions();</script>
<template> <text v-if="status?.granted">Location allowed</text> <button v-else title="Allow location" @press="requestPermission()" /></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { ForegroundPermissionsService } from '@symbiote-native/location/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` @if (location()?.granted) { <text>Location allowed</text> } @else { <button title="Allow location" (press)="request()" /> } `,})export class LocationGate { private readonly service = inject(ForegroundPermissionsService); readonly location = this.service.connect();
request(): void { void this.service.request(); }}connect() returns a signal of the current status; get() and request() are imperative.
<script lang="ts"> import { useForegroundPermissions } from '@symbiote-native/location/svelte';
const location = useForegroundPermissions();</script>
{#if location.status?.granted} <text>Location allowed</text>{:else} <button title="Allow location" onPress={() => location.requestPermission()} />{/if}Svelte returns { status, requestPermission, getPermission } instead of a tuple, matching this
repo’s rune idiom.
import { useForegroundPermissions } from '@symbiote-native/location/solid';
export function LocationGate() { const [status, requestPermission] = useForegroundPermissions();
return status()?.granted ? ( <text>Location allowed</text> ) : ( <button title="Allow location" onPress={() => requestPermission()} /> );}Background updates and geofencing
Section titled “Background updates and geofencing”These register a task defined with @symbiote-native/task-manager’s
defineTask. Define the task at module scope, since native can relaunch the JS bundle headlessly
to run it:
// index.ts, alongside AppRegistry.registerComponentimport { defineTask } from '@symbiote-native/task-manager';
const SYNC_TASK = 'background-location-sync';
defineTask(SYNC_TASK, async ({ data, error }) => { if (error) return; const { locations } = data as { locations: unknown[] }; await syncLocations(locations);});Then start it once the background permission is granted:
import { requestBackgroundPermissionsAsync, startLocationUpdatesAsync,} from '@symbiote-native/location';
const { granted } = await requestBackgroundPermissionsAsync();if (granted) { await startLocationUpdatesAsync(SYNC_TASK, { distanceInterval: 100 });}Position and heading
Section titled “Position and heading”| Signature | Description |
|---|---|
getCurrentPositionAsync(options?) |
Resolves the current position as an ILocationObject |
getLastKnownPositionAsync(options?) |
Resolves the last known position, or null. Filter with maxAge and requiredAccuracy |
watchPositionAsync(options, callback, errorHandler?) |
Calls callback as the position changes. Resolves a subscription; call remove() to stop |
getHeadingAsync() |
Resolves the current compass heading |
watchHeadingAsync(callback, errorHandler?) |
Calls callback as the heading changes. Resolves a subscription |
hasServicesEnabledAsync() |
Whether location services are turned on |
getProviderStatusAsync() |
Resolves the state of the device’s location providers |
enableNetworkProviderAsync() |
Android only. Prompts the user to turn on network location |
installWebGeolocationPolyfill() |
Makes navigator.geolocation call these functions, for libraries that expect it |
ILocationOptions takes accuracy (the Accuracy enum, Lowest to BestForNavigation),
distanceInterval in meters, and on Android timeInterval in milliseconds and
mayShowUserSettingsDialog.
Geocoding
Section titled “Geocoding”| Signature | Description |
|---|---|
geocodeAsync(address) |
Resolves the coordinates matching an address string |
reverseGeocodeAsync(location) |
Resolves the addresses matching coordinates |
Permissions
Section titled “Permissions”| Signature | Description |
|---|---|
getForegroundPermissionsAsync() / requestForegroundPermissionsAsync() |
Read or prompt for while-using-the-app access |
getBackgroundPermissionsAsync() / requestBackgroundPermissionsAsync() |
Read or prompt for always access. Ask for foreground first |
getMotionActivityPermissionsAsync() / requestMotionActivityPermissionsAsync() |
Read or prompt for motion activity |
| Adapter | useForegroundPermissions / useBackgroundPermissions / useMotionActivityPermissions |
|---|---|
| React | [status, requestPermission, getPermission] |
| Vue | [status, requestPermission, getPermission], with status a ref |
| Solid | [status, requestPermission, getPermission], with status an accessor |
| Svelte | { status, requestPermission, getPermission } |
| Angular | ForegroundPermissionsService, BackgroundPermissionsService, MotionActivityPermissionsService |
Motion activity
Section titled “Motion activity”| Signature | Description |
|---|---|
getMotionActivityAsync() |
Resolves the current motion activity (walking, running, driving and so on) |
watchMotionActivityAsync(callback, errorHandler?) |
Calls callback as the activity changes. Foreground only |
Background updates and geofencing
Section titled “Background updates and geofencing”| Signature | Description |
|---|---|
isBackgroundLocationAvailableAsync() |
Whether background location is available on this device |
startLocationUpdatesAsync(taskName, options?) |
Starts delivering locations to a task |
stopLocationUpdatesAsync(taskName) / hasStartedLocationUpdatesAsync(taskName) |
Stop updates, or check whether they are running |
startGeofencingAsync(taskName, regions) |
Starts monitoring regions and delivers enter and exit events to a task |
stopGeofencingAsync(taskName) / hasStartedGeofencingAsync(taskName) |
Stop monitoring, or check whether it is running |
The package also exports the Accuracy, ActivityType, GeofencingEventType and
GeofencingRegionState enums and the ILocation* and IMotionActivity* types.
- Ask foreground first, then background. Background permission is a separate, stricter prompt. On iOS a user who picks Allow Once for while-using gets access for the current session only.
- Background tracking has hard limits. It stops if the user terminates the app and resumes when they reopen it. On Android a terminated app does not restart on a location or geofence event; on iOS the system relaunches the app for a new geofence event. Removing the app from the recents list behaves differently per device vendor.
- Motion activity needs no location permission. It reads the platform’s activity recognition,
gated only by
getMotionActivityPermissionsAsyncandrequestMotionActivityPermissionsAsync. watchMotionActivityAsyncis foreground only. Updates pause while the app is backgrounded and resume when it returns.- Emulators need a location set. In the Android Emulator, enable Settings > Location > Use location and turn off Improve Location Accuracy to feed GPS data. In the iOS Simulator, pick any Features > Location option other than None.
- No Expo Go warning. Upstream’s one-time background-location warning for Expo Go is dropped: this project never runs under Expo Go.
Common questions
Section titled “Common questions”getCurrentPositionAsync hangs on some Android devices. This is a widely reported problem: the
first fix may work and later calls never return, usually on devices with weak or slow location
providers. Do not wait forever. Race the call against your own timer, fall back to
getLastKnownPositionAsync(), and prefer watchPositionAsync when you need updates.
Location stops updating in the background after a few minutes on Android. The system puts the
device to sleep. Pass the foregroundService option to startLocationUpdatesAsync, so Android keeps
the tracking alive with a visible notification.
Background permission is denied right after I ask. Ask foreground permission first, then background. If the user chose Allow Once for while-using access, asking for background permission in the same session silently fails and reports denied. iOS cannot tell you whether the user chose Allow Once.
How accurate does it need to be? Higher accuracy uses more battery. Use Accuracy.Balanced for
most things, and High or BestForNavigation only while it is visibly needed.
How do I test location in an emulator? Android: Settings > Location > Use location, and turn off Improve Location Accuracy to feed GPS data. iOS Simulator: Features > Location, any option but None.
Does background tracking survive the user killing the app? No. It stops when the user terminates the app. On Android a terminated app does not restart for a location event; on iOS the system relaunches it for a geofence event.
Sources: Expo docs: Location, expo/expo#33981 getCurrentPositionAsync and permissions delays on some Android devices, expo/expo#39851 getCurrentPositionAsync hangs indefinitely, expo/expo#26825 getCurrentPositionAsync never returns the location, expo/expo#14076 location stops updating after 5 to 10 minutes on Android.
How the wrapper works
Section titled “How the wrapper works”expo-location’s JS is hand-ported into this package’s core/ (functions, the watch-subscription
machinery shared by position, heading and motion watches, the geolocation polyfill), resolving the
native module through expo-modules-core rather than the expo meta-package. Each adapter
re-exports core plus its own permission hooks, bound to a shared createPermissionHook factory.
The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see
the native setup guide).