Dataniedziela, 9 sierpnia 2026 Czas06:47:16
← Aplikacje mobilne (.NET MAUI)

Lekcja 25. Dostosowanie aplikacji .NET MAUI do platformy i rozmiaru ekranu

Trudny / egzaminacyjny

Po co się tego uczymy?

Jedną z największych zalet .NET MAUI jest to, że ten sam kod działa na telefonie, tablecie, Androidzie, iOS i Windows — ale "działa" nie zawsze oznacza "wygląda dobrze" na każdym z nich. Ta lekcja pokazuje trzy poziomy dostosowania interfejsu: różne wartości dla różnych platform, odczyt informacji o urządzeniu, oraz zmianę CAŁEGO układu strony w zależności od szerokości ekranu (telefon vs. tablet).

Teoria

Poziom 1 — OnPlatform. Znacznik XAML pozwalający podać RÓŻNE wartości tej samej właściwości dla różnych platform, bez pisania osobnego kodu C#: <OnPlatform x:TypeArguments="Thickness"><On Platform="Android" Value="18" /><On Platform="iOS" Value="18,50,18,18" /><On Platform="WinUI" Value="30" /></OnPlatform> — w tym przykładzie iOS dostaje dodatkowy margines górny (50), bo urządzenia z iOS często mają "notch" (wcięcie w ekranie) lub pasek statusu wymagający dodatkowego odstępu, którego Android nie potrzebuje w tym samym stopniu.

Poziom 2 — DeviceInfo. Klasa dostarczająca informacji o URZĄDZENIU w czasie działania aplikacji (nie w XAML, tylko w kodzie C#): DeviceInfo.Current.Platform (Android/iOS/WinUI...), DeviceInfo.Current.Idiom (typ urządzenia: Phone/Tablet/Desktop), DeviceInfo.Current.Model (model konkretnego urządzenia), DeviceInfo.Current.DeviceType (Physical — prawdziwe urządzenie, czy Virtual — emulator/symulator). Te informacje przydają się np. do logowania diagnostycznego albo włączania/wyłączania niektórych funkcji na konkretnych typach urządzeń.

Poziom 3 — reakcja na ROZMIAR ekranu (nie tylko platformę). Kluczowa zasada z tej lekcji: NIE należy zakładać, że "Android" oznacza telefon, a "iOS" oznacza mały ekran — istnieją tablety z Androidem i iPady z iOS, a nawet aplikacje MAUI na Windows mogą być uruchamiane w oknie o dowolnym, zmiennym rozmiarze. O UKŁADZIE strony powinien decydować rzeczywisty, aktualny rozmiar okna, nie nazwa systemu operacyjnego. Dwa mechanizmy realizujące to podejście:

  • VisualStateManager z AdaptiveTrigger — deklaratywny sposób w XAML: definiujemy różne "stany wizualne" (np. "Telefon" dla szerokości od 0, "Tablet" dla szerokości od 700 pikseli) z RÓŻNYMI ustawieniami właściwości (Setter) — system automatycznie przełącza aktywny stan, gdy szerokość okna przekroczy próg zdefiniowany w AdaptiveTrigger.MinWindowWidth.
  • Zdarzenie SizeChanged strony — podejście z kodu C#: nasłuchujemy zmiany rozmiaru strony i RĘCZNIE zmieniamy właściwości układu (np. Grid.SetRow/Grid.SetColumn dla konkretnego elementu), gdy szerokość przekroczy próg. Przydatne, gdy potrzebna logika jest zbyt złożona na deklaratywne triggery XAML.

W praktyce oba podejścia można stosować razem — VisualStateManager do zmiany układu siatki głównej, a kod C# do bardziej szczegółowej logiki przestawiania konkretnych elementów.

Schemat

Szerokość okna aplikacji
        │
   < 700px ──┴── >= 700px
   │                │
   ▼                ▼
Stan "Telefon"   Stan "Tablet"
ColumnDefinitions="*"    ColumnDefinitions="280,*"
RowDefinitions="Auto,*"  RowDefinitions="*"
(panel informacji         (panel informacji i panel
 NAD panelem treści)       treści OBOK siebie)

Równolegle:
SizeChanged (kod C#)
        │
        ▼
Width >= 700? → Grid.SetRow/SetColumn(TrescPanel, ...)
              (ręczne dostrojenie konkretnego elementu)

DeviceInfo.Current → Platform, Idiom, Model, DeviceType
              (informacyjnie, np. do logów albo warunkowej logiki)

Przykład z życia

Aplikacja z pocztą elektroniczną na telefonie pokazuje listę wiadomości na całym ekranie, a po otwarciu wiadomości przechodzi na osobny ekran szczegółów — ale ta sama aplikacja na tablecie (szerszy ekran) może pokazywać listę i szczegóły JEDNOCZEŚNIE, obok siebie, bo jest wystarczająco miejsca na oba panele naraz. To dokładnie ten wzorzec (zmiana układu Grid między "jedna kolumna" a "dwie kolumny") realizuje poniższy przykład.

MainPage.xaml

<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ResponsywnaMauiApp.MainPage"
             Title="Responsywny ekran">

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="Android" Value="18" />
            <On Platform="iOS" Value="18,50,18,18" />
            <On Platform="WinUI" Value="30" />
        </OnPlatform>
    </ContentPage.Padding>

    <Grid x:Name="GlownyGrid" RowDefinitions="Auto,*" ColumnDefinitions="*"
          RowSpacing="16" ColumnSpacing="16">

        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="SzerokoscEkranu">
                <VisualState Name="Telefon">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="ColumnDefinitions" Value="*" />
                        <Setter Property="RowDefinitions" Value="Auto,*" />
                    </VisualState.Setters>
                </VisualState>

                <VisualState Name="Tablet">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="700" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="ColumnDefinitions" Value="280,*" />
                        <Setter Property="RowDefinitions" Value="*" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Border Padding="18" Stroke="#808080">
            <VerticalStackLayout>
                <Label Text="Urządzenie" FontAttributes="Bold" />
                <Label x:Name="UrzadzenieLabel" />
            </VerticalStackLayout>
        </Border>

        <Border x:Name="TrescPanel" Grid.Row="1" Padding="18" Stroke="#808080">
            <VerticalStackLayout>
                <Label Text="Treść aplikacji" FontSize="24" FontAttributes="Bold" />
                <Label Text="Na telefonie panel jest pod informacjami. Na szerokim ekranie znajduje się obok." />
            </VerticalStackLayout>
        </Border>
    </Grid>
</ContentPage>

MainPage.xaml.cs

// MainPage.xaml.cs
using Microsoft.Maui.Devices;

namespace ResponsywnaMauiApp;

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        SizeChanged += OnSizeChanged;
        PokazInformacje();
    }

    private void PokazInformacje()
    {
        UrzadzenieLabel.Text =
            $"Platforma: {DeviceInfo.Current.Platform}n" +
            $"Typ: {DeviceInfo.Current.Idiom}n" +
            $"Model: {DeviceInfo.Current.Model}n" +
            $"System: {DeviceInfo.Current.VersionString}n" +
            $"Emulator: {DeviceInfo.Current.DeviceType == DeviceType.Virtual}";
    }

    private void OnSizeChanged(object? sender, EventArgs e)
    {
        bool szerokiEkran = Width >= 700;

        Grid.SetRow(TrescPanel, szerokiEkran ?  : 1);
        Grid.SetColumn(TrescPanel, szerokiEkran ? 1 : );
    }
}

Komentarz i wyjaśnienie kodu

VisualStateManager w XAML i zdarzenie SizeChanged w kodzie C# działają NIEZALEŻNIE od siebie w tym przykładzie — pierwszy przełącza definicje kolumn/wierszy CAŁEGO Grid, drugi dodatkowo przestawia KONKRETNY element (TrescPanel) na inną pozycję w siatce. W realnym projekcie warto zdecydować się na JEDNO spójne podejście dla danego fragmentu interfejsu, żeby uniknąć sytuacji, w której oba mechanizmy próbują ustawić tę samą właściwość w sprzeczny sposób.

DeviceInfo.Current.DeviceType == DeviceType.Virtual to przydatny sposób odróżnienia testowania na EMULATORZE od prawdziwego urządzenia fizycznego — niektóre funkcje (jak prawdziwy GPS czy aparat) działają inaczej lub wcale na emulatorze, więc taka informacja bywa pomocna przy debugowaniu.

Ćwiczenie samodzielne

Uruchom powyższy przykład na emulatorze i ręcznie zmień rozmiar okna aplikacji (jeśli testujesz na Windows) albo porównaj wygląd na emulatorze telefonu i tabletu — sprawdź, czy układ faktycznie przełącza się między "panel nad panelem" a "panele obok siebie" w okolicach szerokości 700 pikseli.

Zadania do pracy własnej

  1. Zastosuj OnPlatform do koloru tła głównego Grid — inny (delikatnie) odcień dla Androida i inny dla iOS/Windows.

  2. Dodaj TRZECI stan wizualny "SzerokiEkran" (np. od 1000 pikseli) z jeszcze innym układem (np. trzykolumnowym), rozszerzając VisualStateGroup o dodatkowy VisualState z własnym AdaptiveTrigger.

  3. Zbuduj aplikację z listą elementów (np. wiadomości albo notatek z wcześniejszych lekcji) w stylu "master-detail": na wąskim ekranie (telefon) kliknięcie elementu listy przechodzi na OSOBNĄ stronę ze szczegółami (przez nawigację z lekcji 11), a na szerokim ekranie (tablet) lista i szczegóły wybranego elementu są pokazywane RÓWNOCZEŚNIE, obok siebie, bez przechodzenia na osobną stronę — wykorzystaj do tego kombinację VisualStateManager i logiki sprawdzającej bieżącą szerokość ekranu.

Typowe błędy

Zakładanie z góry, że dana platforma = dany typ urządzenia (np. "Android zawsze oznacza telefon") — to błędne założenie ostrzegane wprost w tej lekcji; zawsze sprawdzać rzeczywisty rozmiar ekranu/okna, nie nazwę systemu operacyjnego.

Testowanie WYŁĄCZNIE na jednym typie urządzenia/emulatora podczas tworzenia responsywnego interfejsu — bez sprawdzenia na węższym i szerszym ekranie łatwo przeoczyć, że układ "łamie się" albo wygląda źle na drugim krańcu spektrum rozmiarów.

Nadmierne komplikowanie prostych różnic platformowych kodem C#, gdy wystarczyłby prosty OnPlatform w XAML — dla zwykłych różnic w wartościach liczbowych (marginesy, rozmiary) deklaratywny OnPlatform jest czytelniejszy niż warunki if (DeviceInfo.Platform == ...) w kodzie.

Nawiązanie do egzaminu zawodowego

To bezpośrednia realizacja wymagania INF.04.6.2 "dostosowanie do platformy" — coraz ważniejszy temat w miarę jak aplikacje mobilne działają na coraz bardziej zróżnicowanych urządzeniach (od małych telefonów po duże tablety i składane ekrany). To też naturalne dopełnienie wcześniejszych lekcji o layoutach — te same kontenery (Grid, VerticalStackLayout) teraz reagują dynamicznie na warunki środowiska, zamiast mieć raz ustawiony, sztywny układ.