Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

GPS

GitHub GitHub stars for shinyorg/shiny
Downloads NuGet downloads for Shiny.Gps
Frameworks
.NET MAUI
Blazor
Operating Systems
Android
iOS
Windows
Web

Shiny makes realtime background location updates easy on MAUI. A Blazor WebAssembly implementation for foreground GPS in the browser is part of Shiny.Gps too.

Feature iOS Android Windows Blazor (Web)
Foreground GPS Full Full Full Full
Background GPS Full Full (foreground service) Not Supported Not Supported
Geofencing Full Full Not Supported Not Supported
Changed files
  • MyApp/
Delegates/MyGpsDelegate.cs
MyGpsDelegate.cs
1using Shiny.Locations;2​3namespace MyApp.Delegates;4​5public partial class MyGpsDelegate : GpsDelegate6{7    public MyGpsDelegate(ILogger<MyGpsDelegate> logger) : base(logger)8    {9        // settings as you need10        // this.MinimumDistance = Distance.FromMeters(200);11        // this.MinimumTime = TimeSpan.FromSeconds(15);12    }13​14    protected override async Task OnGpsReading(GpsReading reading)15    {16    }17}18​19#if ANDROID20public partial class MyGpsDelegate : IAndroidForegroundServiceDelegate21{22    public void Configure(AndroidX.Core.App.NotificationCompat.Builder builder)23    {24        25    }26}27#endif

Install Shiny.Gps and register it in your Program.cs - on WebAssembly AddGps() uses the browser Geolocation API:

using Shiny;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// ...
builder.Services.AddGps();
// or with a (foreground-only) delegate:
builder.Services.AddGps<MyGpsDelegate>();

Then inject IGpsManager into your component just like on MAUI:

@inject IGpsManager GpsService
@code {
protected override async Task OnInitializedAsync()
{
var status = await GpsService.RequestAccess(GpsRequest.Foreground);
if (status == AccessState.Available)
await GpsService.StartListener(GpsRequest.Foreground);
}
}

The first call to RequestAccess will trigger the browser’s permission prompt. A working sample lives at samples/Sample.Blazor/Pages/Gps.razor.

IGpsManager gpsManager; // injected, resolved, etc
var request = GpsRequest.Background; // or GpsRequest.Foreground, GpsRequest.Realtime(true)
var access = await gpsManager.RequestAccess(request);
if (access == AccessState.Available)
await gpsManager.StartListener(request);
await gpsManager.StopListener();

StartListener throws if a listener is already running. Check gpsManager.CurrentListener == null (or the IsListening() extension) first.

A listener started with GpsBackgroundMode.Standard or Realtime is saved to Shiny’s settings store. When the OS ends the process and the app starts again, Shiny restarts that listener at startup if the request has AutoRestart set (the default), and CurrentListener returns it again. StopListener() clears it. Foreground-only listeners (GpsBackgroundMode.None) are not saved, so they end with the process. If the restart fails, for example because Android won’t allow a Realtime foreground service to start from the background, the saved listener is cleared and a warning is logged.

First, create the delegate that implements IGpsDelegate

public partial class MyGpsDelegate : Shiny.Locations.IGpsDelegate
{
public MyGpsDelegate()
{
// like all other shiny delegates, dependency injection works here
// treat this as a singleton
}
public Task OnReading(GpsReading reading)
{
// do something with the reading
return Task.CompletedTask;
}
}
#if ANDROID
public partial class MyGpsDelegate : Shiny.IAndroidForegroundServiceDelegate
{
public void Configure(AndroidX.Core.App.NotificationCompat.Builder builder)
{
builder
.SetContentTitle("MyApp")
.SetContentText("My App is following you!!")
.SetSmallIcon(Resource.Mipmap.youricon);
}
}
#endif

Next, let’s register that with your app builder/hosting/service collection

services.AddGps<MyGpsDelegate>();

Lastly, start it up with a background request. On Android, Realtime runs the foreground service whose notification you configured above.

IGpsManager gpsManager; // injected, resolved, etc
var request = GpsRequest.Realtime(true);
var access = await gpsManager.RequestAccess(request);
if (access == AccessState.Available)
await gpsManager.StartListener(request);

All GPS providers automatically detect when the user is stationary and set GpsReading.IsStationary on each reading before it reaches your delegate or the GpsReadingReceived event.

  • iOS 18+ uses native CLLocationUpdater stationary detection provided by the OS.
  • iOS legacy and Android use a distance + time threshold algorithm: if the user moves less than a configurable number of meters within a configurable number of seconds, they are marked stationary.

The default thresholds are 10 meters and 30 seconds. You can configure these via the platform-specific request types:

#if ANDROID
var request = new AndroidGpsRequest(
BackgroundMode: GpsBackgroundMode.Realtime,
StationaryMetersThreshold: 15,
StationarySecondsThreshold: 60
);
#elif IOS
var request = new AppleGpsRequest(
BackgroundMode: GpsBackgroundMode.Realtime,
StationaryMetersThreshold: 15,
StationarySecondsThreshold: 60
);
#endif

Then check the reading in your delegate or via the foreground GpsReadingReceived event:

gpsManager.GpsReadingReceived += (sender, reading) =>
{
if (reading.IsStationary)
{
// user is stationary
}
};

Rx has been removed from Shiny.Locations — GpsReadingReceived is a plain C# event EventHandler<GpsReading>. Remember to -= your handler on disappear/dispose.

The platform mechanics don’t always play by the rules for time & distance filters that you may expect especially with a more ‘real time’ setup.
To work around these issues, you can use the GpsDelegate class which will handle the filtering for you.

When both MinimumDistance and MinimumTime are set, both conditions must be met before OnGpsReading fires. If only one is set, that single condition is used.

public class MyGpsDelegate : GpsDelegate
{
public MyGpsDelegate(ILogger<MyGpsDelegate> logger) : base(logger)
{
// BOTH of these must be satisfied for OnGpsReading to fire
this.MinimumDistance = Distance.FromMeters(200);
this.MinimumTime = TimeSpan.FromMinutes(1);
}
protected override Task OnGpsReading(GpsReading reading)
{
// do something with the filtered reading
return Task.CompletedTask;
}
}

MaximumDistance and MaximumTime act as safety nets. If either maximum threshold is crossed, OnGpsReading fires immediately regardless of whether the minimum conditions are met. This ensures readings are never suppressed for too long.

public class MyGpsDelegate : GpsDelegate
{
public MyGpsDelegate(ILogger<MyGpsDelegate> logger) : base(logger)
{
// Both minimums must be met (AND)
this.MinimumDistance = Distance.FromMeters(200);
this.MinimumTime = TimeSpan.FromMinutes(1);
// Either maximum crossed will always fire (OR), overriding minimums
this.MaximumDistance = Distance.FromKilometers(2);
this.MaximumTime = TimeSpan.FromMinutes(10);
}
protected override Task OnGpsReading(GpsReading reading)
{
// do something with the filtered reading
return Task.CompletedTask;
}
}
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-locations Skill