Lekcja 25. Dostosowanie aplikacji .NET MAUI do platformy i rozmiaru ekranu
Trudny / egzaminacyjnyPo 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:
VisualStateManagerzAdaptiveTrigger— 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 wAdaptiveTrigger.MinWindowWidth.- Zdarzenie
SizeChangedstrony — podejście z kodu C#: nasłuchujemy zmiany rozmiaru strony i RĘCZNIE zmieniamy właściwości układu (np.Grid.SetRow/Grid.SetColumndla 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
Zastosuj
OnPlatformdo koloru tła głównegoGrid— inny (delikatnie) odcień dla Androida i inny dla iOS/Windows.Dodaj TRZECI stan wizualny "SzerokiEkran" (np. od 1000 pikseli) z jeszcze innym układem (np. trzykolumnowym), rozszerzając
VisualStateGroupo dodatkowyVisualStatez własnymAdaptiveTrigger.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ę
VisualStateManageri 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.