Lekcja 20. REST API w .NET MAUI — pobieranie danych z internetu
Trudny / egzaminacyjnyPo co się tego uczymy?
Prawdziwe aplikacje mobilne rzadko działają w izolacji — pobierają dane z serwera (lista produktów, wiadomości, pogoda), a często też wysyłają dane z powrotem. Ta lekcja pokazuje, jak aplikacja MAUI komunikuje się z serwerem przez REST API: wysyła żądanie HTTP, odbiera odpowiedź w formacie JSON i zamienia ją na obiekty C# gotowe do wyświetlenia na liście.
Teoria
REST API to sposób komunikacji między aplikacją a serwerem przez zwykłe żądania HTTP — dokładnie te same metody, których używa przeglądarka: GET pobiera dane, POST wysyła nowy obiekt, PUT/PATCH aktualizuje, DELETE usuwa. Serwer odpowiada danymi w formacie JSON, który w C# zamieniamy na obiekty przez deserializację — dokładnie tę samą technikę, którą poznałeś przy zapisie plików JSON na dysku, tylko teraz dane przychodzą przez sieć zamiast z pliku.
Klasa HttpClient to wbudowany w .NET mechanizm wysyłania żądań HTTP. Kluczowa zasada: HttpClient powinien być polem klasy (tworzonym RAZ), a NIE tworzony od nowa przy każdym zapytaniu — tworzenie wielu instancji HttpClient w krótkim czasie może prowadzić do wyczerpania dostępnych portów sieciowych systemu. Ustawienie BaseAddress pozwala potem odwoływać się do zasobów krótkimi, względnymi ścieżkami (np. "posts?_limit=10") zamiast pełnych adresów URL za każdym razem.
Uprawnienie do internetu na Androidzie. W przeciwieństwie do Windows, aplikacja na Androidzie musi JAWNIE zadeklarować, że potrzebuje dostępu do sieci — w pliku Platforms/Android/AndroidManifest.xml dodaje się <uses-permission android:name="android.permission.INTERNET" />. Bez tej deklaracji WSZYSTKIE żądania sieciowe zawiodą na urządzeniu z Androidem, nawet jeśli kod C# jest całkowicie poprawny.
Sprawdzanie połączenia z internetem. Zanim w ogóle spróbujemy wysłać żądanie, dobra praktyka to sprawdzenie Connectivity.Current.NetworkAccess — jeśli urządzenie nie ma dostępu do internetu, lepiej pokazać jasny komunikat niż czekać na przekroczenie limitu czasu żądania sieciowego.
Odczyt odpowiedzi. Metoda response.EnsureSuccessStatusCode() rzuca wyjątek, jeśli serwer odpowiedział kodem błędu (np. 404, 500) — dzięki temu błędna odpowiedź serwera jest wykrywana od razu, zamiast próbować (nieudanie) sparsować treść błędu jako listę danych. response.Content.ReadFromJsonAsync<List<Post>>() to skrót łączący odczyt treści odpowiedzi i deserializację JSON w jednym wywołaniu.
Schemat
Button "Pobierz dane" (Clicked)
│
▼
Connectivity.Current.NetworkAccess == Internet?
│
nie ─┴─ tak
│ │
▼ ▼
DisplayAlert PostService.PobierzPostyAsync()
"Brak │
internetu" │ HttpClient.GetAsync("posts?_limit=10")
▼
serwer REST (jsonplaceholder.typicode.com)
│
│ odpowiedź: JSON (lista postów)
▼
response.EnsureSuccessStatusCode()
│
▼
ReadFromJsonAsync<List<Post>>()
│
▼
ListaPostow.ItemsSource = wynik
│
▼
CollectionView pokazuje listę
Przykład z życia
Aplikacja z wiadomościami pobiera najnowsze artykuły z serwera redakcji przy każdym uruchomieniu, aplikacja pogodowa pobiera aktualną prognozę z zewnętrznego serwisu, a aplikacja sklepowa pobiera aktualny katalog produktów — we wszystkich tych przypadkach aplikacja mobilna jest tylko "witryną" wyświetlającą dane, które faktycznie żyją na serwerze i mogą się zmieniać niezależnie od tego, kiedy użytkownik ostatnio aktualizował aplikację.
MainPage.xaml + AndroidManifest.xml
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ApiMauiApp.MainPage"
Title="REST API">
<Grid Padding="20" RowDefinitions="Auto,Auto,*" RowSpacing="14">
<Label Text="Wpisy pobrane z REST API" FontSize="24" FontAttributes="Bold" />
<Button Grid.Row="1" Text="Pobierz dane" Clicked="Pobierz_Clicked" />
<CollectionView Grid.Row="2" x:Name="ListaPostow"
EmptyView="Brak danych. Kliknij przycisk pobierania.">
<CollectionView.ItemTemplate>
<DataTemplate>
<Border Margin="0,5" Padding="12" Stroke="#808080">
<VerticalStackLayout>
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Label Text="{Binding Body}" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
<!-- Platforms/Android/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application android:allowBackup="true" android:supportsRtl="true"></application>
</manifest>
Modele/Post.cs + Serwisy/PostService.cs + MainPage.xaml.cs
// Modele/Post.cs
namespace ApiMauiApp.Modele;
public class Post
{
public int UserId { get; set; }
public int Id { get; set; }
public string Title { get; set; } = "";
public string Body { get; set; } = "";
}
// ---------------------------------------------------------------------
// Serwisy/PostService.cs
using System.Net.Http.Json;
using ApiMauiApp.Modele;
namespace ApiMauiApp.Serwisy;
public class PostService
{
private readonly HttpClient _httpClient = new()
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
public async Task<List<Post>> PobierzPostyAsync()
{
using HttpResponseMessage response = await _httpClient.GetAsync("posts?_limit=10");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<Post>>() ?? new List<Post>();
}
}
// ---------------------------------------------------------------------
// MainPage.xaml.cs
using ApiMauiApp.Serwisy;
using Microsoft.Maui.Networking;
namespace ApiMauiApp;
public partial class MainPage : ContentPage
{
private readonly PostService _postService = new();
public MainPage()
{
InitializeComponent();
}
private async void Pobierz_Clicked(object sender, EventArgs e)
{
if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
{
await DisplayAlert("Brak internetu", "Połącz urządzenie z internetem i spróbuj ponownie.", "OK");
return;
}
try
{
ListaPostow.ItemsSource = await _postService.PobierzPostyAsync();
}
catch (HttpRequestException ex)
{
await DisplayAlert("Błąd API", ex.Message, "OK");
}
catch (Exception ex)
{
await DisplayAlert("Błąd", ex.Message, "OK");
}
}
}
Komentarz i wyjaśnienie kodu
PostService to osobna klasa skupiająca CAŁĄ komunikację sieciową — dokładnie ta sama zasada oddzielenia logiki od UI, co w lekcji o MVVM i w lekcji o bazach danych. Strona MainPage nie wie NIC o adresach URL ani o formacie JSON, tylko woła _postService.PobierzPostyAsync() i dostaje gotową listę obiektów Post.
W Pobierz_Clicked zwróć uwagę na blok try/catch z DWOMA typami wyjątków: HttpRequestException (specyficzny błąd sieciowy/HTTP, np. serwer nieosiągalny albo zła odpowiedź) obsłużony osobno z bardziej precyzyjnym komunikatem, oraz ogólny Exception jako zabezpieczenie na wszelki inny, nieprzewidziany błąd (np. błąd podczas deserializacji JSON).
Ćwiczenie samodzielne
Utwórz nowy projekt MAUI z powyższym przykładem (publiczne, darmowe API JSONPlaceholder służy właśnie do nauki i nie wymaga żadnego klucza dostępu). Uruchom na emulatorze Androida, sprawdź działanie przycisku "Pobierz dane", a następnie WYŁĄCZ internet w emulatorze i sprawdź, czy komunikat o braku internetu poprawnie się pojawia.
Zadania do pracy własnej
Dodaj
ActivityIndicatorwidoczny podczas pobierania danych (ustawIsRunning = trueprzed wywołaniem serwisu ifalsepo jego zakończeniu, najlepiej w blokufinally).Dodaj pole
Entrydo wpisania ID użytkownika i zmieńPobierzPostyAsynctak, by przyjmowała parametr i pobierała tylko wpisy TEGO konkretnego użytkownika (adres"posts?userId={id}").Dodaj formularz (Tytuł + Treść) i nową metodę
DodajPostAsync(Post nowyPost)wPostService, wysyłającą dane metodąPOST(_httpClient.PostAsJsonAsync("posts", nowyPost)) — po pomyślnym wysłaniu pokaż użytkownikowi otrzymaną z serwera odpowiedź (JSONPlaceholder symuluje zapis i zwraca nowo "utworzony" obiekt z przypisanym ID).
Typowe błędy
Brak uprawnienia INTERNET w AndroidManifest.xml — kod C# jest całkowicie poprawny, ale każde żądanie sieciowe kończy się błędem wyłącznie na urządzeniu z Androidem (na Windows może działać, co bywa mylące podczas testowania).
Tworzenie nowego HttpClient przy każdym żądaniu zamiast trzymania jednej instancji jako pole klasy — przy częstym powtarzaniu może prowadzić do wyczerpania portów sieciowych systemu operacyjnego.
Brak obsługi braku internetu i błędów serwera — aplikacja bez sprawdzenia Connectivity i bloku try/catch po prostu "zawiesza się" albo wyświetla nieobsłużony wyjątek, gdy urządzenie jest offline albo serwer nie odpowiada.
Zakładanie z góry sukcesu operacji sieciowej — operacje przez internet mogą zawieść z wielu powodów niezależnych od kodu aplikacji (przerwane połączenie, przeciążony serwer, zmiana adresu API) i ZAWSZE trzeba to uwzględnić w kodzie.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja wymagania INF.04.6.2 "pobieranie/wysyłanie danych z internetu" — jeden z najbardziej praktycznych elementów działu aplikacji mobilnych, bo niemal każda nowoczesna aplikacja komunikuje się z jakimś serwerem. Publiczne API JSONPlaceholder służy wyłącznie do nauki — w prawdziwej aplikacji adres API, sposób autoryzacji i format danych zależą od konkretnego serwera, z którym aplikacja współpracuje.