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.

Ta interakcja wymaga włączonego JavaScriptu.

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.

PlikRola w ćwiczeniu
Serwisy/IPowiadomieniaService.csLogika i obsługa danych
Platforms/Android/AndroidPowiadomieniaService.csLogika i obsługa danych
AndroidManifest.xml (fragment)Fragment w istniejącym pliku
MauiProgram.cs (fragment)Fragment w istniejącym pliku
App.xaml.csLogika i obsługa danych
MainPage.xamlWygląd strony lub kontrolki
MainPage.xaml.csLogika 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.

MainPage.xaml — etap tymczasowyxaml
<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>
  1. 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

ElementPo 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_NOTIFICATIONSOd Androida 13: wpis w manifeście oraz Permissions.RequestAsync<Permissions.PostNotifications>().
NotificationCompat.BuilderSkłada powiadomienie: tytuł, treść, ikona, SetAutoCancel(true), akcja po kliknięciu.
PendingIntentZapakowana akcja na później: kliknięcie powiadomienia otwiera MainActivity.
Android < 13 nie wymaga zgody, ale wywołanie `RequestAsync` jest tam bezpieczne.

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.

Serwisy/IPowiadomieniaService.cscsharp
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.
    }
}
  1. interface IPowiadomieniaService Kontrakt: co aplikacja potrafi, bez szczegółów jak.
  2. BrakPowiadomienService Implementacja zastępcza. Bez niej aplikacja na iOS nie uruchomi się, bo kontener DI nie znajdzie usługi wymaganej przez MainPage.

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.

Platforms/Android/AndroidPowiadomieniaService.cscsharp
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);
    }
}
  1. _id = 100 i Notify(_id++, ...) Każde powiadomienie ma inny numer, więc się kumulują. Ten sam numer zastępuje poprzednie.
  2. Platform.AppContext Kontekst aplikacji Androida, potrzebny do większości jego API.
  3. UtworzKanal i SdkInt < O Na Androidzie starszym niż 8 kanałów nie ma, więc metoda kończy się od razu.
  4. PendingIntent.GetActivity(... Immutable) Akcja uruchomiona po kliknięciu. Flaga Immutable jest wymagana przez nowsze wersje Androida.
  5. 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ę.

AndroidManifest.xml (fragment)xml
<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.

MauiProgram.cs (fragment)csharp
#if ANDROID
builder.Services.AddSingleton<
    PowiadomieniaMauiApp.Serwisy.IPowiadomieniaService,
    PowiadomieniaMauiApp.Platforms.Android.AndroidPowiadomieniaService>();
#else
builder.Services.AddSingleton<
    PowiadomieniaMauiApp.Serwisy.IPowiadomieniaService,
    PowiadomieniaMauiApp.Serwisy.BrakPowiadomienService>();
#endif
  1. #if ANDROID ... #else ... #endif Kompilacja warunkowa: kod z #if ANDROID jest w ogóle pomijany dla innych platform, więc odwołania do klas Androida nie psują budowania na iOS.
  2. 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.

App.xaml.cscsharp
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.

MainPage.xamlxaml
<?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>
  1. 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.

MainPage.xaml.cscsharp
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
    }
}
  1. MainPage(IPowiadomieniaService powiadomienia) Wstrzykiwanie zależności: strona zna tylko interfejs. Dodanie iOS zmieni wyłącznie rejestrację.
  2. Permissions.RequestAsync<Permissions.PostNotifications>() Okno zgody na Androidzie 13+. Brak Granted kończy metodę komunikatem.
  3. TytulEntry.Text ?? "Przypomnienie" Wartość domyślna, gdy pole jest puste.
  4. #else z DisplayAlert Na 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ń
Tak nie
// brak UtworzKanal(context)
new NotificationCompat.Builder(context, KanalId)...

Na nowszych Androidach powiadomienie jest po cichu ignorowane, bez błędu.

Tak
UtworzKanal(context);

Kanał musi istnieć przed wysłaniem.

To samo id dla wszystkich
Tak nie
NotificationManagerCompat.From(context).Notify(100, notification);

Każde kolejne powiadomienie zastępuje poprzednie.

Tak
NotificationManagerCompat.From(context).Notify(_id++, notification);

Różne id oznaczają osobne karty.

Pominięta zgoda na Androidzie 13+
Tak nie
_powiadomienia.Wyslij(tytul, tresc);

System może wyciszyć powiadomienia bez żadnego komunikatu w kodzie.

Tak
var status = await Permissions.RequestAsync<Permissions.PostNotifications>();
if (status != PermissionStatus.Granted) return;

Zgoda jest jawna.

Kod Androida poza folderem Platforms bez #if
Tak nie
var m = new Android.App.NotificationChannel(...);   // w MainPage.xaml.cs

Błąd kompilacji na iOS i Windows, które nie znają klas Androida.

Tak
#if ANDROID
   ...
#endif

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

Oczekiwany rezultat
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.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Powiadomienia różnią się między systemami, więc stosujemy interfejs wspólny + implementację platformową.
  2. Android: kanał, zgoda POST_NOTIFICATIONS (13+) i NotificationCompat.Builder z PendingIntent.
  3. Każde powiadomienie z innym id, inaczej zastępuje poprzednie.
  4. #if ANDROID ogranicza kod do jednej platformy, a rejestracja w MauiProgram.cs wybiera implementację.
  5. 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ę →
Źródła i dalsza nauka