Dataczwartek, 13 sierpnia 2026 Czas17:50:21
← Aplikacje mobilne (.NET MAUI)

Lekcja 21. GPS i lokalizacja w .NET MAUI

Trudny / egzaminacyjny

Po 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

  1. Dodaj przycisk "Zapisz lokalizację", który zapisuje ostatnio pobrane współrzędne do pliku JSON (technika z lekcji o plikach TXT/JSON).

  2. 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.

  3. 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.