
Maui Geolocation
- 30 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Adds geolocation to .NET MAUI apps using Microsoft.Maui.Devices.Sensors, covering one-shot and continuous location and platform permissions.
About
Guides adding geolocation to .NET MAUI apps via Microsoft.Maui.Devices.Sensors, covering one-shot and continuous location and platform permissions. A developer uses it when reading device location in a MAUI app.
- One-shot and continuous location
- Platform permissions for Android, iOS, macOS, Windows
Maui Geolocation by the numbers
- 30 all-time installs (skills.sh)
- Ranked #665 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-geolocationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Adds geolocation to .NET MAUI apps using Microsoft.Maui.Devices.Sensors, covering one-shot and continuous location and platform permissions.
Files
.NET MAUI Geolocation
Critical: Always Pass a CancellationToken
GetLocationAsync can hang indefinitely if GPS is off, the device is indoors, or permissions are in a pending state. Always set a timeout.
// ❌ Hangs forever if no GPS fix is available
var location = await Geolocation.Default.GetLocationAsync(
new GeolocationRequest(GeolocationAccuracy.High));
// ✅ Times out after 30 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var location = await Geolocation.Default.GetLocationAsync(
new GeolocationRequest(GeolocationAccuracy.High), cts.Token);---
Accuracy vs. Battery Trade-off
Use the lowest accuracy that satisfies your feature. Higher accuracy drains battery significantly faster — especially on Android.
| Use case | Recommended accuracy |
|---|---|
| City-level / weather | Lowest or Low |
| Nearby search / store finder | Medium |
| Turn-by-turn navigation | High or Best |
---
Platform Gotchas
iOS 14 reduced accuracy
Users can grant "approximate" location (accuracy > 1 km). Your app receives a location, but it may be useless for precision features.
- Check
location.Accuracy— values > 100 m likely indicate reduced precision. - Use
GeolocationRequest.RequestFullAccuracywith a key matching
NSLocationTemporaryUsageDescriptionDictionary in Info.plist to prompt for full accuracy.
Android: mock locations in security-sensitive flows
// ⚠️ Always check in security-sensitive flows (e.g., geofence, check-in)
if (location.IsFromMockProvider)
{
// Reject — user is spoofing location
}Android: Altitude 0.0 is not sea level
Some Android devices return 0.0 for Altitude when the GPS has no barometric sensor. Treat 0.0 as "unknown", not sea level.
Android 10+: background location requires separate permission
ACCESS_BACKGROUND_LOCATION must be requested separately from foreground permissions and triggers a distinct system dialog. Requesting it together with foreground permissions causes both to be denied on some devices.
GetLastKnownLocationAsync returns null
This is expected on first boot or after a location-data reset. Always fall back to GetLocationAsync:
// ✅ Cache-first with fresh fallback
var location = await geolocation.GetLastKnownLocationAsync();
if (location is null || location.Timestamp < DateTimeOffset.UtcNow.AddMinutes(-5))
{
location = await geolocation.GetLocationAsync(
new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10)), ct);
}Permissions must be requested before any geolocation call
Call Permissions.RequestAsync<Permissions.LocationWhenInUse>() first, or catch PermissionException. Calling GetLocationAsync without permission throws on some platforms and returns null on others — inconsistent behaviour across platforms.
---
Continuous Listening: Don't Forget to Unsubscribe
Failing to remove the LocationChanged handler when stopping causes continued GPS usage and battery drain.
// ❌ Forgetting to unsubscribe — GPS stays active
public void StopTracking()
{
Geolocation.Default.StopListeningForeground();
// LocationChanged handler still fires!
}
// ✅ Always remove the event handler
public void StopTracking()
{
Geolocation.Default.StopListeningForeground();
Geolocation.Default.LocationChanged -= OnLocationChanged;
}---
Checklist
- [ ]
CancellationTokenpassed to everyGetLocationAsynccall - [ ] Accuracy level matched to feature need (not blindly set to
Best) - [ ]
GetLastKnownLocationAsyncnull-check withGetLocationAsyncfallback - [ ] Runtime permissions requested before first geolocation call
- [ ]
LocationChangedhandler removed when stopping continuous listening - [ ] Android:
ACCESS_BACKGROUND_LOCATIONrequested separately (if needed) - [ ] iOS:
NSLocationTemporaryUsageDescriptionDictionaryadded for full-accuracy prompt - [ ] Android:
IsFromMockProviderchecked in security-sensitive flows
Geolocation API Reference
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
}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 |
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);
}
}