Lekcja 23. Powiadomienia lokalne w .NET MAUI — wersja Android
Trudny / egzaminacyjnyPo co się tego uczymy?
Powiadomienia to jeden z najbardziej rozpoznawalnych elementów aplikacji mobilnych — przypomnienie o zadaniu, potwierdzenie zamówienia, alarm. Ta lekcja pokazuje kompletny, działający mechanizm powiadomień lokalnych (wyświetlanych bez serwera, bezpośrednio przez samą aplikację) na Androidzie, wraz z wymaganą od Androida 13 zgodą użytkownika.
Teoria
Dlaczego akurat kod platformowy? Powiadomienia lokalne są obsługiwane zupełnie INACZEJ przez każdy system operacyjny (Android, iOS, Windows) — MAUI nie ma jednego uniwersalnego API dla tej funkcji. Rozwiązaniem jest standardowy wzorzec: definiujemy WSPÓLNY interfejs (IPowiadomieniaService) używany przez resztę aplikacji, a każda platforma dostaje WŁASNĄ implementację tego interfejsu, umieszczoną w odpowiednim folderze Platforms/. Ta lekcja realizuje kompletną, prostą implementację dla Androida — ten sam wzorzec (interfejs + implementacja platformowa) stosuje się analogicznie dla iOS czy Windows, gdyby zaszła taka potrzeba.
Kanał powiadomień (Notification Channel). Od Androida 8 (Oreo) każde powiadomienie MUSI należeć do jakiegoś "kanału" — kanały pozwalają użytkownikowi kontrolować różne typy powiadomień OSOBNO (np. wyłączyć powiadomienia marketingowe, ale zostawić włączone przypomnienia). Kanał tworzy się RAZ (metoda sprawdza Build.VERSION.SdkInt, żeby nie próbować tworzyć kanału na starszych wersjach Androida, gdzie ta koncepcja nie istnieje) i nadaje mu unikalny identyfikator, nazwę widoczną dla użytkownika w ustawieniach systemowych, oraz poziom ważności (NotificationImportance).
Zgoda na powiadomienia (Android 13+). Podobnie jak przy lokalizacji, od Androida 13 aplikacja musi poprosić o JAWNĄ zgodę użytkownika, zanim będzie mogła wyświetlać powiadomienia — uprawnienie POST_NOTIFICATIONS deklarowane w manifeście PLUS wywołanie w kodzie await Permissions.RequestAsync<Permissions.PostNotifications>(). Na starszych wersjach Androida (przed 13) ta zgoda nie jest wymagana, ale wywołanie RequestAsync jest bezpieczne do wykonania niezależnie od wersji systemu.
Budowanie powiadomienia. NotificationCompat.Builder to klasa z biblioteki AndroidX pozwalająca "poskładać" powiadomienie krok po kroku: SetContentTitle/SetContentText (treść), SetSmallIcon (ikona w pasku statusu), SetAutoCancel(true) (powiadomienie znika automatycznie po kliknięciu), SetContentIntent(pendingIntent) (co się stanie po kliknięciu powiadomienia — tutaj: otwarcie głównej aktywności aplikacji). PendingIntent to "zapakowana" akcja do wykonania w PRZYSZŁOŚCI (gdy użytkownik kliknie powiadomienie, które może pojawić się długo po tym, jak aplikacja była aktywna).
Rejestracja w kontenerze Dependency Injection. W MauiProgram.cs, w bloku warunkowej kompilacji #if ANDROID ... #endif (kod wewnątrz kompiluje się TYLKO dla Androida), rejestrujemy konkretną implementację pod wspólnym interfejsem: builder.Services.AddSingleton<IPowiadomieniaService, AndroidPowiadomieniaService>(). Dzięki temu strona (MainPage) może przyjąć IPowiadomieniaService jako parametr konstruktora, nie wiedząc NIC o konkretnej platformie — to mechanizm Dependency Injection, standardowy sposób odseparowania kodu wspólnego od platformowego w MAUI.
Schemat
MauiProgram.cs
│
│ #if ANDROID
│ AddSingleton<IPowiadomieniaService, AndroidPowiadomieniaService>()
│ #endif
▼
MainPage(IPowiadomieniaService powiadomienia) ← wstrzyknięte przez DI
│
│ Button "Wyślij powiadomienie" (Clicked)
▼
#if ANDROID
Permissions.RequestAsync<PostNotifications>()
│
odmowa ─┴─ zgoda
│ │
▼ ▼
DisplayAlert _powiadomienia.Wyslij(tytul, wiadomosc)
"Brak zgody" │
▼
AndroidPowiadomieniaService.Wyslij(...)
│
├── UtworzKanal(context) (raz, jeśli SDK >= Oreo)
├── NotificationCompat.Builder(...).Build()
└── NotificationManagerCompat.Notify(id, notification)
│
▼
powiadomienie widoczne w pasku systemowym
Przykład z życia
Aplikacja do nauki języków może przypominać "Czas na dzisiejszą lekcję!", aplikacja z listą zadań może powiadamiać o zbliżającym się terminie, a aplikacja sklepowa może informować o statusie zamówienia — wszystkie te scenariusze korzystają z tego samego mechanizmu: kanał powiadomień, zgoda użytkownika, i zbudowanie powiadomienia z tytułem, treścią i akcją po kliknięciu.
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="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>
<!-- Platforms/Android/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
IPowiadomieniaService.cs + AndroidPowiadomieniaService.cs + MauiProgram.cs + MainPage.xaml.cs
// Serwisy/IPowiadomieniaService.cs - wspólny interfejs
namespace PowiadomieniaMauiApp.Serwisy;
public interface IPowiadomieniaService
{
void Wyslij(string tytul, string wiadomosc);
}
// ---------------------------------------------------------------------
// Platforms/Android/AndroidPowiadomieniaService.cs - implementacja dla Androida
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);
}
}
// ---------------------------------------------------------------------
// MauiProgram.cs - fragment przed return builder.Build()
#if ANDROID
builder.Services.AddSingleton<
PowiadomieniaMauiApp.Serwisy.IPowiadomieniaService,
PowiadomieniaMauiApp.Platforms.Android.AndroidPowiadomieniaService>();
#endif
// ---------------------------------------------------------------------
// MainPage.xaml.cs
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 DisplayAlert("Brak zgody", "Zezwól aplikacji na wyświetlanie powiadomień.", "OK");
return;
}
_powiadomienia.Wyslij(
TytulEntry.Text ?? "Przypomnienie",
WiadomoscEditor.Text ?? "Wiadomość z aplikacji MAUI");
#else
await DisplayAlert("Platforma", "Ten przykład został przygotowany dla Androida.", "OK");
#endif
}
}
Komentarz i wyjaśnienie kodu
Zwróć uwagę na wzorzec "interfejs wspólny + implementacja platformowa" — MainPage zna TYLKO IPowiadomieniaService (metodę Wyslij(tytul, wiadomosc)), nie wie NIC o NotificationCompat czy kanałach powiadomień Androida. Gdyby w przyszłości dodać implementację dla iOS, kod MainPage nie zmieniłby się WCALE — zmieniłaby się tylko rejestracja w MauiProgram.cs.
Pole _id (inkrementowane przy każdym wywołaniu: _id++) zapewnia, że KAŻDE powiadomienie ma inny numer identyfikacyjny — gdyby wszystkie powiadomienia miały ten sam id, każde kolejne NADPISYWAŁOBY poprzednie w pasku powiadomień, zamiast się kumulować.
Dyrektywy #if ANDROID ... #else ... #endif to kompilacja warunkowa — kod wewnątrz #if ANDROID w ogóle NIE jest kompilowany dla innych platform, więc odwołania do klas specyficznych dla Androida (jak Permissions.PostNotifications, dostępne tylko od pewnej wersji) nie powodują błędów kompilacji na iOS czy Windows.
Ćwiczenie samodzielne
Utwórz projekt z powyższym przykładem, uruchom na emulatorze Androida (najlepiej z Androidem 13 lub nowszym, żeby zobaczyć okno z prośbą o zgodę), wyślij powiadomienie i sprawdź, że kliknięcie na nie otwiera aplikację.
Zadania do pracy własnej
Dodaj licznik wysłanych powiadomień (pole
intwMainPage, zwiększane po każdym udanym wysłaniu) wyświetlany w dodatkowej etykiecie.Zapisz domyślny tytuł powiadomienia w
Preferences(z poprzedniej lekcji), tak żeby poleTytulEntrybyło automatycznie wypełnione ostatnio użytym tytułem przy każdym starcie aplikacji.Rozbuduj projekt o wybór KONKRETNEJ godziny (
TimePicker) i zaplanowanie powiadomienia na tę godzinę w przyszłości zamiast natychmiastowego wysłania — będzie to wymagało dodatkowego mechanizmu Androida (AlarmManageriBroadcastReceiver), więc potraktuj to jako zadanie eksploracyjne z samodzielnym wyszukaniem dokumentacji.
Typowe błędy
Brak utworzenia kanału powiadomień przed próbą wysłania powiadomienia na nowszych Androidach — bez kanału system może po prostu zignorować powiadomienie bez żadnego widocznego błędu.
Używanie tego samego id dla wszystkich powiadomień — powoduje, że każde kolejne powiadomienie zastępuje poprzednie zamiast się kumulować w pasku powiadomień.
Zakładanie, że zgoda na powiadomienia jest automatyczna na Androidzie 13 i nowszych — bez jawnego Permissions.RequestAsync<PostNotifications>() powiadomienia mogą być całkowicie wyciszone przez system, bez żadnego komunikatu o błędzie w kodzie aplikacji.
Pisanie kodu specyficznego dla Androida bez dyrektyw #if ANDROID w kodzie wspólnym projektu (poza folderem Platforms/Android) — spowoduje błąd kompilacji dla pozostałych platform (iOS, Windows), które nie znają klas Androida.
Nawiązanie do egzaminu zawodowego
To realizacja wymagania INF.04.6.2 "powiadamianie", wymienionego wprost jako przykładowa prosta aplikacja mobilna w podstawie programowej. Wzorzec interfejsu wspólnego z implementacją platformową, poznany w tej lekcji, to fundamentalna technika w MAUI, przydatna wszędzie tam, gdzie funkcja urządzenia różni się między systemami operacyjnymi.