Aplikacje mobilne · Powiadomienia
Powiadomienia lokalne w .NET MAUI (Android)
Przypomnienie o zadaniu, potwierdzenie zamówienia, alarm: powiadomienia widzi każdy użytkownik telefonu. Lokalne powiadomienia wyświetla sama aplikacja, bez serwera. Każdy system robi to inaczej, więc poznasz wzorzec „wspólny interfejs + implementacja platformowa”.
- około 75 minut
- .NET MAUI · Android · Dependency Injection
- INF.04.6.2
Po tej lekcji: napisać interfejs powiadomień, zaimplementować go na Androidzie (kanał, zgoda, NotificationCompat), zarejestrować w kontenerze DI i użyć w stronie.
Powiadomienie w systemie Android
Komunikat na stronie znika razem z nią. Powiadomienie systemowe trafia poza okno aplikacji, dlatego zależy od zgody użytkownika, kanału i kodu platformy.
Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.
Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji
1 · Sprawdź w działaniu
Kanał, zgoda, identyfikator
Wyślij kilka powiadomień. Potem zepsuj po kolei: brak kanału, zawsze to samo id i brak wpisu w manifeście na Androidzie 13. Zobacz, które błędy nie dają żadnego komunikatu.
Najpierw przewidź, potem sprawdź: W modelu powiadomień przejdź drogę: zgoda → kanał → treść. Wyłącz zgodę i przewidź, dlaczego sam poprawny tekst nie wystarczy.
Najgroźniejsze błędy powiadomień są ciche: system po prostu nic nie pokazuje albo zastępuje poprzednie powiadomienie. Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.
1. Przygotuj miejsce pracy
Co dodajesz, a co podmieniasz?
Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.
| Plik | Rola w ćwiczeniu |
|---|---|
| Serwisy/IPowiadomieniaService.cs | Logika i obsługa danych |
| Platforms/Android/AndroidPowiadomieniaService.cs | Logika i obsługa danych |
| AndroidManifest.xml (fragment) | Fragment w istniejącym pliku |
| MauiProgram.cs (fragment) | Fragment w istniejącym pliku |
| App.xaml.cs | Logika i obsługa danych |
| MainPage.xaml | Wygląd strony lub kontrolki |
| MainPage.xaml.cs | Logika i obsługa danych |
Zanim uruchomisz: trzy połączenia
x:Class="PowiadomieniaMauiApp.NazwaStrony" w XAML musi zgadzać się z namespace PowiadomieniaMauiApp i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.
x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.
Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.
2. Uruchom sam wygląd — jeszcze bez logiki
W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PowiadomieniaMauiApp.MainPage">
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="Przypomnienie" FontSize="24" />
<Entry Placeholder="Tytuł" />
<Editor Placeholder="Treść" HeightRequest="100" />
</VerticalStackLayout>
</ContentPage>
- Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.
Dlaczego kod platformowy
MAUI nie ma jednego API powiadomień
Aplikacja do nauki języków przypomina „Czas na dzisiejszą lekcję!”, lista zadań informuje o terminie, sklep o statusie zamówienia. Mechanizm jest wszędzie ten sam: kanał, zgoda użytkownika, zbudowanie powiadomienia.
Powiadomienia działają jednak zupełnie inaczej na Androidzie, iOS i Windows, a MAUI nie daje jednego uniwersalnego API. Rozwiązanie: wspólny interfejs IPowiadomieniaService dla reszty aplikacji i osobna implementacja w folderze Platforms/ dla każdego systemu. W tej lekcji wykonujemy implementację dla Androida.
2 · Cztery elementy
Z czego składa się powiadomienie Androida
| Element | Po co |
|---|---|
Kanał (NotificationChannel) | Od Androida 8 każde powiadomienie musi należeć do kanału. Użytkownik wyłącza kanały osobno (np. reklamy, zostawiając przypomnienia). |
Zgoda POST_NOTIFICATIONS | Od Androida 13: wpis w manifeście oraz Permissions.RequestAsync<Permissions.PostNotifications>(). |
NotificationCompat.Builder | Składa powiadomienie: tytuł, treść, ikona, SetAutoCancel(true), akcja po kliknięciu. |
PendingIntent | Zapakowana akcja na później: kliknięcie powiadomienia otwiera MainActivity. |
4. Połącz wygląd z działaniem
Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.
Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.
Budowa krok po kroku
Serwisy/IPowiadomieniaService.cs
Interfejs opisuje Wyslij bez wskazania systemu. BrakPowiadomienService jest świadomą implementacją zastępczą, nie obsługą Windows.
Interfejs zna jedną metodę. Reszta aplikacji nie wie nic o Androidzie. Dodatkowa klasa BrakPowiadomienService jest bezpieczną „pustą” implementacją dla pozostałych platform.
namespace PowiadomieniaMauiApp.Serwisy;
public interface IPowiadomieniaService
{
void Wyslij(string tytul, string wiadomosc);
}
// Zastępcza implementacja dla platform bez własnego kodu powiadomień
public class BrakPowiadomienService : IPowiadomieniaService
{
public void Wyslij(string tytul, string wiadomosc)
{
// Ta platforma nie obsługuje jeszcze powiadomień z tej lekcji.
}
}
interface IPowiadomieniaServiceKontrakt: co aplikacja potrafi, bez szczegółów jak.BrakPowiadomienServiceImplementacja zastępcza. Bez niej aplikacja na iOS nie uruchomi się, bo kontener DI nie znajdzie usługi wymaganej przezMainPage.
Budowa krok po kroku
Platforms/Android/AndroidPowiadomieniaService.cs
Plik musi pozostać w Platforms/Android. Android8+ potrzebuje kanału; identyfikator powiadomienia pozwala systemowi odróżniać kolejne wpisy.
Ta klasa istnieje tylko w folderze Androida, więc może używać klas Androida. W tej lekcji nie kompilujemy jej na komputerze bez pakietu Android. Uruchom ją na emulatorze, zgodnie z ćwiczeniem.
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using Microsoft.Maui.ApplicationModel;
using PowiadomieniaMauiApp.Serwisy;
namespace PowiadomieniaMauiApp.Platforms.Android;
public class AndroidPowiadomieniaService : IPowiadomieniaService
{
private const string KanalId = "przypomnienia";
private const string KanalNazwa = "Przypomnienia";
private int _id = 100;
public void Wyslij(string tytul, string wiadomosc)
{
Context context = Platform.AppContext;
UtworzKanal(context);
Intent intent = new(context, typeof(MainActivity));
PendingIntent pendingIntent = PendingIntent.GetActivity(
context, _id, intent,
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
var notification = new NotificationCompat.Builder(context, KanalId)
.SetContentTitle(tytul)
.SetContentText(wiadomosc)
.SetSmallIcon(global::Android.Resource.Drawable.IcDialogInfo)
.SetAutoCancel(true)
.SetContentIntent(pendingIntent)
.Build();
NotificationManagerCompat.From(context).Notify(_id++, notification);
}
private static void UtworzKanal(Context context)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O) return;
var kanal = new NotificationChannel(KanalId, KanalNazwa, NotificationImportance.Default)
{
Description = "Powiadomienia i przypomnienia aplikacji"
};
var manager = (NotificationManager?)context.GetSystemService(Context.NotificationService);
manager?.CreateNotificationChannel(kanal);
}
}
_id = 100iNotify(_id++, ...)Każde powiadomienie ma inny numer, więc się kumulują. Ten sam numer zastępuje poprzednie.Platform.AppContextKontekst aplikacji Androida, potrzebny do większości jego API.UtworzKanaliSdkInt < ONa Androidzie starszym niż 8 kanałów nie ma, więc metoda kończy się od razu.PendingIntent.GetActivity(... Immutable)Akcja uruchomiona po kliknięciu. FlagaImmutablejest wymagana przez nowsze wersje Androida.NotificationCompat.Builder(...)Składa tytuł, treść, ikonę i akcję.SetAutoCancel(true)usuwa powiadomienie po kliknięciu.
Budowa krok po kroku
Platforms/Android/AndroidManifest.xml
Dopisz POST_NOTIFICATIONS do istniejącego manifest poza application. Sama deklaracja nie zastępuje pytania użytkownika o zgodę w czasie działania.
Bez tego wpisu Android 13+ nawet nie wyświetli okna z prośbą o zgodę.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
Budowa krok po kroku
MauiProgram.cs: przed return builder.Build()
Wklej rejestrację do CreateMauiApp przed return builder.Build(). Dopisz także builder.Services.AddTransient<MainPage>();. #if ANDROID wybiera implementację podczas kompilacji, nie podczas kliknięcia.
Kontener DI dostaje właściwą implementację dla bieżącej platformy.
#if ANDROID
builder.Services.AddSingleton<
PowiadomieniaMauiApp.Serwisy.IPowiadomieniaService,
PowiadomieniaMauiApp.Platforms.Android.AndroidPowiadomieniaService>();
#else
builder.Services.AddSingleton<
PowiadomieniaMauiApp.Serwisy.IPowiadomieniaService,
PowiadomieniaMauiApp.Serwisy.BrakPowiadomienService>();
#endif
#if ANDROID ... #else ... #endifKompilacja warunkowa: kod z#if ANDROIDjest w ogóle pomijany dla innych platform, więc odwołania do klas Androida nie psują budowania na iOS.AddSingleton<Interfejs, Implementacja>()Strona poprosi o interfejs, a kontener poda implementację.
Budowa krok po kroku
Uruchom stronę z kontenera DI
App(MainPage strona) korzysta z rejestracji DI. Nie twórz MainPage bez wymaganego serwisu.
W MauiProgram.cs dodaj też builder.Services.AddTransient<MainPage>(); przed builder.Build(). Kontener utworzy stronę i poda IPowiadomieniaService do konstruktora. Nie używaj new MainPage() bez argumentu.
namespace PowiadomieniaMauiApp;
public partial class App : Application
{
private readonly MainPage _strona;
public App(MainPage strona)
{
InitializeComponent();
_strona = strona;
}
protected override Window CreateWindow(IActivationState? activationState)
=> new Window(_strona);
}
Budowa krok po kroku
MainPage.xaml
Dodaj lub otwórz MainPage.xaml i podmień jego zawartość. Najpierw odszukaj kontener, potem nazwane kontrolki oraz wskazane zdarzenia. Pozostaw zgodne x:Class i nazwę klasy w pliku .xaml.cs.
Tytuł, treść i przycisk.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PowiadomieniaMauiApp.MainPage"
Title="Powiadomienia">
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="Lokalne powiadomienie" FontSize="24" FontAttributes="Bold" />
<Entry x:Name="TytulEntry" Placeholder="Tytuł" />
<Editor x:Name="WiadomoscEditor" Placeholder="Treść wiadomości" HeightRequest="120" />
<Button Text="Wyślij powiadomienie" Clicked="Wyslij_Clicked" />
</VerticalStackLayout>
</ContentPage>
- Przeczytaj powiązanie x:Name identyfikuje kontrolkę, a Binding odczytuje właściwość obiektu ustawionego jako BindingContext. To dwa różne mechanizmy.
Budowa krok po kroku
MainPage.xaml.cs
To handler tylko dla Androida. Najpierw wynik Permissions.RequestAsync, potem Wyslij. Kod #else wyjaśnia ograniczenie na pozostałych platformach.
Strona dostaje usługę przez konstruktor, prosi o zgodę i wysyła powiadomienie.
using Microsoft.Maui.ApplicationModel;
using PowiadomieniaMauiApp.Serwisy;
namespace PowiadomieniaMauiApp;
public partial class MainPage : ContentPage
{
private readonly IPowiadomieniaService _powiadomienia;
public MainPage(IPowiadomieniaService powiadomienia)
{
InitializeComponent();
_powiadomienia = powiadomienia;
}
private async void Wyslij_Clicked(object sender, EventArgs e)
{
#if ANDROID
PermissionStatus status = await Permissions.RequestAsync<Permissions.PostNotifications>();
if (status != PermissionStatus.Granted)
{
await DisplayAlertAsync("Brak zgody", "Zezwól aplikacji na wyświetlanie powiadomień.", "OK");
return;
}
_powiadomienia.Wyslij(
TytulEntry.Text ?? "Przypomnienie",
WiadomoscEditor.Text ?? "Wiadomość z aplikacji MAUI");
#else
await DisplayAlertAsync("Platforma", "Ten przykład został przygotowany dla Androida.", "OK");
#endif
}
}
MainPage(IPowiadomieniaService powiadomienia)Wstrzykiwanie zależności: strona zna tylko interfejs. Dodanie iOS zmieni wyłącznie rejestrację.Permissions.RequestAsync<Permissions.PostNotifications>()Okno zgody na Androidzie 13+. BrakGrantedkończy metodę komunikatem.TytulEntry.Text ?? "Przypomnienie"Wartość domyślna, gdy pole jest puste.#elsezDisplayAlertNa innych platformach pokazujemy komunikat zamiast wywoływać kod Androida.
5. Odbiór: czy naprawdę działa?
Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.
9 · Czego unikać
Cztery ciche porażki
Brak kanału powiadomień
// brak UtworzKanal(context)
new NotificationCompat.Builder(context, KanalId)...Na nowszych Androidach powiadomienie jest po cichu ignorowane, bez błędu.
UtworzKanal(context);Kanał musi istnieć przed wysłaniem.
To samo id dla wszystkich
NotificationManagerCompat.From(context).Notify(100, notification);Każde kolejne powiadomienie zastępuje poprzednie.
NotificationManagerCompat.From(context).Notify(_id++, notification);Różne id oznaczają osobne karty.
Pominięta zgoda na Androidzie 13+
_powiadomienia.Wyslij(tytul, tresc);System może wyciszyć powiadomienia bez żadnego komunikatu w kodzie.
var status = await Permissions.RequestAsync<Permissions.PostNotifications>();
if (status != PermissionStatus.Granted) return;Zgoda jest jawna.
Kod Androida poza folderem Platforms bez #if
var m = new Android.App.NotificationChannel(...); // w MainPage.xaml.csBłąd kompilacji na iOS i Windows, które nie znają klas Androida.
#if ANDROID
...
#endifKompilacja warunkowa zawęża kod do jednej platformy.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Zastąp puste tytuły stałym napisem Przypomnienie.
Zastąp puste tytuły stałym napisem Przypomnienie. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Operator ?? obsługuje null, ale nie tekst z samych spacji.
Krok 2 — pokaż podpowiedź
Sprawdź string.IsNullOrWhiteSpace przed wywołaniem serwisu.
Krok 3 — pokaż podpowiedź
Przetestuj null/puste pole, spacje oraz zwykły tytuł.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
10 · Sprawdź się
Która część odpowiada za co?
Cztery pytania.
Podsumowanie
Co warto zapamiętać
- Powiadomienia różnią się między systemami, więc stosujemy interfejs wspólny + implementację platformową.
- Android: kanał, zgoda
POST_NOTIFICATIONS(13+) iNotificationCompat.BuilderzPendingIntent. - Każde powiadomienie z innym id, inaczej zastępuje poprzednie.
#if ANDROIDogranicza kod do jednej platformy, a rejestracja wMauiProgram.cswybiera implementację.- Dla pozostałych platform zarejestruj implementację zastępczą, aby aplikacja się uruchamiała.
Dalej na swoim koncie
Rozgrzewki, quiz i zadania są w biurku ucznia.
Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.
Zaloguj się →