Lekcja 21. GPS i lokalizacja w .NET MAUI
Trudny / egzaminacyjnyPo co się tego uczymy?
Wiele aplikacji mobilnych korzysta z lokalizacji urządzenia — nawigacja, aplikacje pogodowe, wyszukiwanie najbliższych punktów usługowych. Ta lekcja pokazuje, jak poprawnie poprosić użytkownika o zgodę na dostęp do lokalizacji (co jest OBOWIĄZKOWE ze względów prywatności), pobrać aktualne współrzędne GPS i obliczyć odległość do wybranego punktu.
Teoria
Uprawnienia (Permissions) — fundament dostępu do funkcji urządzenia. Lokalizacja to dana wrażliwa, więc system operacyjny WYMAGA jawnej zgody użytkownika, zanim aplikacja będzie mogła jej użyć. W MAUI proces wygląda tak: (1) deklaracja w plikach platformowych (Android: AndroidManifest.xml z uprawnieniami ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION; iOS: Info.plist z opisem NSLocationWhenInUseUsageDescription — tekst, który zobaczy użytkownik w oknie z prośbą o zgodę), (2) w kodzie C# wywołanie await Permissions.RequestAsync<Permissions.LocationWhenInUse>(), które pokazuje systemowe okno z pytaniem o zgodę i zwraca PermissionStatus — jeśli status jest inny niż Granted, aplikacja NIE powinna nawet próbować pobrać lokalizacji, tylko poinformować użytkownika.
Pobieranie lokalizacji — klasa Geolocation. Geolocation.Default.GetLocationAsync(request, token) zwraca obiekt Location z właściwościami Latitude (szerokość geograficzna), Longitude (długość geograficzna) i Accuracy (dokładność pomiaru w metrach). GeolocationRequest pozwala określić żądaną dokładność (GeolocationAccuracy.Medium/High...) — WYŻSZA dokładność oznacza zwykle WIĘKSZE zużycie baterii, więc warto dobierać ją do faktycznej potrzeby aplikacji.
CancellationTokenSource — zabezpieczenie przed zawieszeniem. Pobranie lokalizacji może potencjalnie trwać bardzo długo (np. w budynku, gdzie sygnał GPS jest słaby) — CancellationTokenSource(TimeSpan) automatycznie przerywa oczekiwanie po zadanym czasie, żeby aplikacja nie "zawisła" w nieskończoność czekając na pomiar, który może nigdy nie nadejść.
Obliczanie odległości. Statyczna metoda Location.CalculateDistance(punktA, punktB, DistanceUnits.Kilometers) oblicza odległość między dwoma punktami geograficznymi (uwzględniając krzywiznę Ziemi) — bardzo przydatna funkcja, żeby np. sprawdzić, jak daleko użytkownik znajduje się od interesującego go miejsca, bez ręcznego liczenia wzorów geograficznych.
Obsługa specyficznych wyjątków. FeatureNotEnabledException oznacza, że GPS jest fizycznie WYŁĄCZONY w ustawieniach urządzenia (nie problem z uprawnieniami, tylko wyłączona usługa lokalizacji), a PermissionException sygnalizuje brak zgody — rozróżnienie tych dwóch przypadków pozwala pokazać użytkownikowi trafną wskazówkę, co dokładnie trzeba poprawić.
Schemat
Button "Pobierz lokalizację"
│
▼
Permissions.RequestAsync<LocationWhenInUse>()
│
odmowa ─┴─ zgoda
│ │
▼ ▼
DisplayAlert ActivityIndicator.IsRunning = true
"Brak zgody" │
▼
Geolocation.Default.GetLocationAsync(request, token z limitem czasu)
│
┌─────────┼──────────────┐
│ │ │
null (brak sukces wyjątek (GPS wyłączony /
pomiaru) (Location) brak uprawnienia / inny błąd)
│ │ │
▼ ▼ ▼
komunikat WynikLabel + DisplayAlert z odpowiednim
CalculateDistance komunikatem błędu
do Warszawy
Przykład z życia
Aplikacja turystyczna może informować użytkownika, jak daleko znajduje się od najbliższej atrakcji, a aplikacja dostawcza może pokazywać kurierowi odległość do adresu dostawy — obie funkcje opierają się na dokładnie tym samym mechanizmie: prośbie o zgodę, pobraniu współrzędnych GPS i obliczeniu odległości do konkretnego punktu.
MainPage.xaml + AndroidManifest.xml + Info.plist
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="GpsMauiApp.MainPage"
Title="Moja lokalizacja">
<ScrollView>
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="GPS - aktualna lokalizacja" FontSize="24" FontAttributes="Bold" />
<Button Text="Pobierz lokalizację" Clicked="PobierzLokalizacje_Clicked" />
<ActivityIndicator x:Name="Ladowanie" IsVisible="False" IsRunning="False" />
<Label x:Name="WynikLabel" Text="Brak pomiaru." />
<Label x:Name="OdlegloscLabel" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
<!-- Platforms/Android/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
</manifest>
<!-- Platforms/iOS/Info.plist - wewnątrz <dict> -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Lokalizacja jest potrzebna do pokazania współrzędnych użytkownika.</string>
MainPage.xaml.cs
// MainPage.xaml.cs
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Devices.Sensors;
namespace GpsMauiApp;
public partial class MainPage : ContentPage
{
private readonly Location _warszawa = new(52.2297, 21.0122);
public MainPage()
{
InitializeComponent();
}
private async void PobierzLokalizacje_Clicked(object sender, EventArgs e)
{
PermissionStatus status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
await DisplayAlert("Brak zgody", "Bez zgody aplikacja nie może pobrać lokalizacji.", "OK");
return;
}
try
{
Ladowanie.IsVisible = true;
Ladowanie.IsRunning = true;
var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
using var anulowanie = new CancellationTokenSource(TimeSpan.FromSeconds(12));
Location? lokalizacja = await Geolocation.Default.GetLocationAsync(request, anulowanie.Token);
if (lokalizacja is null)
{
WynikLabel.Text = "Nie udało się ustalić lokalizacji.";
return;
}
WynikLabel.Text =
$"Szerokość: {lokalizacja.Latitude:F6}n" +
$"Długość: {lokalizacja.Longitude:F6}n" +
$"Dokładność: {lokalizacja.Accuracy:F0} m";
double km = Location.CalculateDistance(lokalizacja, _warszawa, DistanceUnits.Kilometers);
OdlegloscLabel.Text = $"Odległość od centrum Warszawy: {km:F1} km";
}
catch (FeatureNotEnabledException)
{
await DisplayAlert("GPS wyłączony", "Włącz lokalizację w ustawieniach urządzenia.", "OK");
}
catch (PermissionException)
{
await DisplayAlert("Brak uprawnienia", "Nadaj aplikacji dostęp do lokalizacji.", "OK");
}
catch (Exception ex)
{
await DisplayAlert("Błąd", ex.Message, "OK");
}
finally
{
Ladowanie.IsRunning = false;
Ladowanie.IsVisible = false;
}
}
}
Komentarz i wyjaśnienie kodu
Blok finally gwarantuje, że wskaźnik ładowania (Ladowanie) zostanie ukryty NIEZALEŻNIE od tego, czy operacja zakończyła się sukcesem, błędem czy wyjątkiem — to ważny wzorzec przy każdej operacji asynchronicznej pokazującej stan "w trakcie ładowania".
Kolejność bloków catch ma znaczenie: NAJPIERW obsługujemy bardziej SZCZEGÓŁOWE wyjątki (FeatureNotEnabledException, PermissionException), a dopiero na końcu ogólny Exception jako "siatkę bezpieczeństwa" na wszystko inne — gdyby kolejność była odwrotna, ogólny catch (Exception ex) przechwyciłby WSZYSTKO, zanim bardziej szczegółowe bloki miałyby szansę zadziałać.
Ćwiczenie samodzielne
Utwórz projekt z powyższym przykładem. Emulator Androida nie musi mieć prawdziwego modułu GPS — w narzędziach emulatora (Extended Controls → Location) możesz ustawić dowolne współrzędne testowe i sprawdzić, czy obliczona odległość od Warszawy się zgadza.
Zadania do pracy własnej
Dodaj przycisk "Zapisz lokalizację", który zapisuje ostatnio pobrane współrzędne do pliku JSON (technika z lekcji o plikach TXT/JSON).
Dodaj pole
Entry(dwie wartości: szerokość i długość) pozwalające wpisać WŁASNY punkt docelowy zamiast sztywno ustawionej Warszawy, i przelicz odległość do tego punktu.Rozbuduj aplikację o informację "jesteś w pobliżu", która porównuje obliczoną odległość z progiem 1 km i wyświetla inny komunikat/kolor etykiety w zależności od tego, czy użytkownik jest bliżej czy dalej niż ten próg — a dodatkowo zapisuje historię wszystkich pomiarów (czas + współrzędne) do listy w pamięci wyświetlanej w
CollectionView.
Typowe błędy
Brak deklaracji uprawnienia w pliku platformowym (manifest Androida / Info.plist iOS) — nawet jeśli kod C# poprawnie woła Permissions.RequestAsync, bez wcześniejszej deklaracji w pliku platformowym system może w ogóle nie pokazać okna z prośbą o zgodę, tylko od razu zwrócić odmowę.
Brak sprawdzenia wyniku PermissionStatus przed próbą pobrania lokalizacji — próba wywołania GetLocationAsync bez zgody zakończy się wyjątkiem PermissionException, którego można łatwo uniknąć, sprawdzając status wcześniej.
Ignorowanie możliwości zwrócenia null przez GetLocationAsync — w słabym zasięgu GPS albo przy przekroczeniu limitu czasu metoda może zwrócić null zamiast rzucić wyjątek; brak sprawdzenia is null prowadzi do NullReferenceException przy próbie odczytania Latitude.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja wymagania INF.04.6.2 "lokalizacja/GPS", wymienionego wprost w podstawie programowej jako przykładowa prosta aplikacja mobilna. Mechanizm uprawnień (Permissions) poznany w tej lekcji jest identyczny dla WSZYSTKICH funkcji wymagających zgody użytkownika (aparat, mikrofon, powiadomienia) — więc ta wiedza przenosi się bezpośrednio na kolejną lekcję o powiadomieniach lokalnych.