packages

Location

Ask for location permission with expo's response shape plus the accuracy the user granted, read the current or last known position at six accuracy levels, watch it live, read the compass heading, and turn an address into coordinates or coordinates into a street address. Android uses the platform LocationManager, so phones without Google Play services work. Background updates and geofencing raise NotSupportedError, because pyxpo has no task manager yet.

  • pyxpo-location
  • v0.1.0a1
  • Native: Swift + Kotlin
  • Like expo-location
Position, accuracy and address on Android and iOS.
Position, accuracy and address on Android and iOS.

Install

Install the package into the same environment as pyxpo.

$pip install pyxpo-location

Add location to modules in your app config, then import it in your code.

pyproject.toml
[tool.pyxpo]
modules = ["location"]
python
import pyxpo_location as Location

Needs Python 3.9+ and pyxpo>=0.1.0a2.

This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.

Platforms

iOSiOS 15+. Position, watching, heading, geocoding, services check. The Simulator has no compass.
AndroidAPI 24+. Everything foreground. No Google Play services needed.
Desktop previewNot supported. Calls raise NotSupportedError; has_services_enabled_async() returns False.
WebSame as the desktop.

Quick start

app/index.py
import pyxpo_location as Location
from pyxpo import Button, Text, View, component, use_async, use_state


@component
def Here():
    wanted, set_wanted = use_state(0)
    place, set_place = use_state("Not yet")

    async def locate():
        if wanted == 0:
            return
        perm = await Location.request_foreground_permissions_async()
        if not perm["granted"]:
            set_place("Permission denied")
            return
        loc = await Location.get_current_position_async(accuracy=Location.Accuracy.HIGH)
        c = loc["coords"]
        set_place("%.4f, %.4f" % (c["latitude"], c["longitude"]))

    use_async(locate, [wanted])
    return View(Text(place), Button("Locate me", lambda: set_wanted(wanted + 1)))

Configuration

These options set the text iOS shows when it asks the user for access. Each has a default, so you only set the ones you want to change.

OptionInfo.plist keyDefault
locationWhenInUsePermissionNSLocationWhenInUseUsageDescriptionUse your location.
locationAlwaysAndWhenInUsePermissionNSLocationAlwaysAndWhenInUseUsageDescriptionUse your location, also while the app is in the background.
pyproject.toml
[tool.pyxpo.module_options.location]
locationWhenInUsePermission = "Use your location."
locationAlwaysAndWhenInUsePermission = "Use your location, also while the app is in the background."

Permissions

pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.

iOSNSLocationWhenInUseUsageDescription, and NSLocationAlwaysAndWhenInUseUsageDescription for background permission. Set the text with the options above.
AndroidACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION, added by the module; location hardware is marked optional. Background needs ACCESS_BACKGROUND_LOCATION in [tool.pyxpo.android] permissions.

API reference

Permissions

The response is expo's {"status", "granted", "canAskAgain", "expires"}, plus ios: {"scope"} ("whenInUse", "always", "none") or android: {"accuracy"} ("fine", "coarse", "none"). An Android user can allow only approximate location, so granted can be True with "coarse".

await get_foreground_permissions_async()→ dict

Reads the permission. Does not ask.

await request_foreground_permissions_async()→ dict

Asks if undetermined.

get_background_permissions_async() / request_background_permissions_async()→ dict

The same for background location. Ask for foreground first. Android 11+ sends the user to Settings.

use_foreground_permissions(get=True, request=False)→ (response, request, get)

A hook. response is None until read. get=True reads on mount, request=True asks on mount. use_background_permissions(...) is the same.

Position

A location is {"coords": {"latitude", "longitude", "altitude", "accuracy", "altitudeAccuracy", "heading", "speed"}, "timestamp", "mocked"}. Unknown values are None, timestamp is milliseconds since 1970, and mocked is Android only. Every function takes expo's options dict or Python keywords.

await get_current_position_async(options=None, **kw)→ location

A fresh fix. accuracy defaults to BALANCED.

await get_last_known_position_async(options=None, **kw)→ location or None

The cached fix. None when there is none, it is older than max_age ms, or less exact than required_accuracy metres.

await watch_position_async(options=None, callback=None, error_handler=None, **kw)→ LocationSubscription

Live updates tuned by accuracy, time_interval (Android, ms) and distance_interval (metres). Remove it when the screen goes away, or it keeps draining the battery.

Heading

await get_heading_async()→ dict

{"trueHeading", "magHeading", "accuracy"} in degrees. trueHeading is -1 until the location is known. Raises without a compass.

await watch_heading_async(callback, error_handler=None)→ LocationSubscription

The heading as a stream.

Geocoding

Both need a network. An empty address, or a latitude or longitude that is not a number or out of range, raises ValueError.

await geocode_async(address)→ list[dict]

[{"latitude", "longitude", "altitude", "accuracy"}].

await reverse_geocode_async(location)→ list[dict]

Addresses with name, street, streetNumber, district, city, subregion, region, postalCode, country, isoCountryCode, timezone and formattedAddress.

Provider status

await has_services_enabled_async()→ bool

True when location services are on. False on the desktop.

await get_provider_status_async()→ dict

locationServicesEnabled, backgroundModeEnabled, and on Android gpsAvailable, networkAvailable, passiveAvailable.

await enable_network_provider_async()→ None

Android: opens the location settings when network location is off.

await is_background_location_available_async()→ bool

Always False for now.

Not supported yet

These need a background task manager, which pyxpo does not have yet. Use watch_position_async while the app is open.

start_location_updates_async, stop_location_updates_async, has_started_location_updates_async→ NotSupportedError

Background location updates.

start_geofencing_async, stop_geofencing_async, has_started_geofencing_async→ NotSupportedError

Geofencing.

Types

Every function also has its expo spelling, such as getCurrentPositionAsync, and options accept expo's keys, such as {"distanceInterval": 10}.

Accuracy (IntEnum, also LocationAccuracy)

KeyTypeDescription
LOWEST1About 3 km.
LOW2About 1 km.
BALANCED3About 100 m. The default.
HIGH4About 10 m.
HIGHEST5The best the device can do.
BEST_FOR_NAVIGATION6The best, plus sensor data on iOS.

Other types

KeyTypeDescription
ActivityTypeIntEnumiOS activity hint: OTHER, AUTOMOTIVE_NAVIGATION, FITNESS, OTHER_NAVIGATION, AIRBORNE.
PermissionStatusstr enumGRANTED, DENIED, UNDETERMINED.
LocationSubscriptionclassremove() stops the updates; safe to call twice.
GeofencingEventType, GeofencingRegionStateIntEnumFor compatibility; unused while geofencing is unsupported.

Errors

KeyTypeDescription
NotSupportedErrorpyxpo.module_apiWeb, desktop, or a background or geofencing call. Catch it and show a manual place picker.
ValueErrorbad argumentBad accuracy, negative max_age, empty address or bad coordinates.
TypeErrorbad callbackThe watch callback is not callable.
native errorExceptionNo permission, services off, or no fix in time. Offer a retry.
ModuleNotLinkedErrorpyxpo.module_api"location" is missing from modules. Add it and rebuild.

Examples

Watch while the screen is open

Start the watch from use_async and remove it when you are done. The Simulator and emulator use a place you set in their location settings.

Locate me on Android and iOS showing latitude, longitude, accuracy, altitude and an address.
Locate me on Android and iOS showing latitude, longitude, accuracy, altitude and an address.
python
import pyxpo_location as Location


async def follow(on_move):
    sub = await Location.watch_position_async(
        {"accuracy": Location.Accuracy.BALANCED, "distanceInterval": 10},
        lambda loc: on_move(loc["coords"]),
    )
    return sub          # call sub.remove() when the screen goes away

Recipe: find stores near me

Ask once, show the place, and fall back to the last known position when a fresh fix is slow.

python
import pyxpo_location as Location
from pyxpo import Button, Text, View, component, use_async, use_state


@component
def NearMe():
    tries, set_tries = use_state(1)
    text, set_text = use_state("Looking...")

    async def find():
        perm = await Location.request_foreground_permissions_async()
        if not perm["granted"]:
            set_text("Allow location to see stores near you.")
            return
        try:
            loc = await Location.get_current_position_async(accuracy=Location.Accuracy.BALANCED)
        except Exception:
            loc = await Location.get_last_known_position_async(max_age=10 * 60_000)
        if loc is None:
            set_text("No position yet. Is location on?")
            return
        c = loc["coords"]
        where = await Location.reverse_geocode_async(
            {"latitude": c["latitude"], "longitude": c["longitude"]})
        city = where[0].get("city") if where else None
        set_text("Stores near %s" % (city or "you"))

    use_async(find, [tries])
    return View(Text(text), Button("Try again", lambda: set_tries(tries + 1)))

Testing

Replace the native call on the desktop host. Argument checks need no fake: accuracy=9 raises ValueError before any native call.

python
import asyncio

import pyxpo_location as Location


def test_current_position(monkeypatch):
    async def fake_call(*args, **kwargs):
        return {"coords": {"latitude": 52.52, "longitude": 13.4, "accuracy": 5}, "timestamp": 0}

    monkeypatch.setattr(Location, "_native", lambda: None)
    monkeypatch.setattr(Location, "call", fake_call)
    loc = asyncio.run(Location.get_current_position_async(accuracy=Location.Accuracy.HIGH))
    assert loc["coords"]["latitude"] == 52.52

Good to know

  • Higher accuracy costs more battery. Indoors, lower the accuracy or fall back to get_last_known_position_async.
  • If no prompt appears, the permission was denied before and canAskAgain is False. Send the user to Settings.
  • For a web build, let the user type an address and use your own geocoding service.
  • use_async(fn, deps) runs again when a dependency changes, so a button that starts async work bumps a counter in state.