maui-geolocation

SKILL.md

.NET MAUI Geolocation

Platform permissions

Android

Add to Platforms/Android/AndroidManifest.xml inside <manifest>:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Android 10+ background location (only if needed) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

iOS

Add to Platforms/iOS/Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to provide nearby results.</string>

For full-accuracy prompts on iOS 14+, also add:

<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
  <key>FullAccuracyUsageKey</key>
  <string>This app needs precise location for turn-by-turn directions.</string>
</dict>

macOS (Mac Catalyst)

Add to Platforms/MacCatalyst/Entitlements.plist:

<key>com.apple.security.personal-information.location</key>
<true/>

Windows

No manifest changes required. Location capability is enabled by default.

Core API — Geolocation.Default

Method Returns Purpose
GetLastKnownLocationAsync() Location? Cached device location (fast, may be stale)
GetLocationAsync(GeolocationRequest, CancellationToken) Location? Fresh GPS fix with desired accuracy
StartListeningForegroundAsync(GeolocationListeningRequest) bool Begin continuous location updates
StopListeningForeground() void Stop continuous updates

One-shot location

try
{
    var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
    var location = await Geolocation.Default.GetLocationAsync(request, cts.Token);

    if (location is null)
    {
        // Location unavailable — GPS off, permissions denied, or timeout
        return;
    }

    Console.WriteLine($"{location.Latitude}, {location.Longitude} ±{location.Accuracy}m");
}
catch (FeatureNotSupportedException)
{
    // Device lacks GPS hardware
}
catch (PermissionException)
{
    // Location permission not granted
}

Always check for null — the method returns null when the device cannot obtain a fix.

Continuous listening

public partial class TrackingViewModel : ObservableObject
{
    [ObservableProperty] Location? currentLocation;

    public async Task StartTracking()
    {
        Geolocation.Default.LocationChanged += OnLocationChanged;
        var request = new GeolocationListeningRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(5));
        var success = await Geolocation.Default.StartListeningForegroundAsync(request);
        if (!success)
            Geolocation.Default.LocationChanged -= OnLocationChanged;
    }

    public void StopTracking()
    {
        Geolocation.Default.StopListeningForeground();
        Geolocation.Default.LocationChanged -= OnLocationChanged;
    }

    void OnLocationChanged(object? sender, GeolocationLocationChangedEventArgs e)
    {
        CurrentLocation = e.Location;
    }
}

GeolocationAccuracy levels

Enum value Android (m) iOS (m) Windows (m)
Lowest 500 3000 1000–5000
Low 500 1000 300–3000
Medium 100–500 100 30–500
High 0–100 10 ≤30
Best 0–100 ~0 ≤10

Higher accuracy consumes more battery. Use the lowest level that satisfies your feature.

CancellationToken pattern

Always pass a CancellationToken to GetLocationAsync to avoid indefinite hangs:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var location = await Geolocation.Default.GetLocationAsync(
    new GeolocationRequest(GeolocationAccuracy.High), cts.Token);

DI-friendly service wrapper

Register IGeolocation in MauiProgram.cs:

builder.Services.AddSingleton<IGeolocation>(Geolocation.Default);
builder.Services.AddSingleton<LocationService>();

Consume via constructor injection:

public class LocationService(IGeolocation geolocation)
{
    public async Task<Location?> GetCurrentAsync(CancellationToken ct = default)
    {
        var cached = await geolocation.GetLastKnownLocationAsync();
        if (cached is not null && cached.Timestamp > DateTimeOffset.UtcNow.AddMinutes(-5))
            return cached;

        return await geolocation.GetLocationAsync(
            new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10)), ct);
    }
}

Platform gotchas

  • iOS 14 reduced accuracy: Users can grant "approximate" location. Check location.Accuracy — values > 100 m likely indicate reduced precision. Use GeolocationRequest.RequestFullAccuracy with a matching key from NSLocationTemporaryUsageDescriptionDictionary to prompt for full accuracy.
  • Mock locations (IsFromMockProvider): On Android, location.IsFromMockProvider is true when a mock-location app is active. Always check this in security-sensitive flows.
  • Altitude 0.0 on Android: Some Android devices return 0.0 for Altitude when GPS has no barometric sensor. Treat 0.0 as "unknown" rather than sea level.
  • Null returns: GetLastKnownLocationAsync returns null on first boot or after a location-data reset. Always fall back to GetLocationAsync.
  • Permissions at runtime: Call Permissions.RequestAsync<Permissions.LocationWhenInUse>() before any geolocation call, or handle PermissionException.
  • Background location on Android 10+: ACCESS_BACKGROUND_LOCATION must be requested separately from foreground permissions and triggers a distinct system dialog.
Weekly Installs
6
GitHub Stars
71
First Seen
13 days ago
Installed on
opencode6
github-copilot6
codex6
kimi-cli6
gemini-cli6
amp6