Aplikacje mobilne · Nawigacja

Nawigacja II: Shell, trasy i przekazywanie parametrów

Shell to dziś zalecany, nowoczesny sposób nawigacji w .NET MAUI. Działa jak mapa aplikacji z zarejestrowanymi trasami, do których przechodzisz po nazwie, a nie przez ręczne tworzenie stron. Daje też gotowe menu boczne i dolne zakładki.

  • około 55 minut
  • .NET MAUI · Shell · GoToAsync · QueryProperty
  • INF.04.6.2

Po tej lekcji: zarejestrować trasę (Routing.RegisterRoute), przejść po nazwie (GoToAsync), przekazać parametry słownikiem i odebrać je przez [QueryProperty] lub IQueryAttributable.

Nawigacja z zakładkami i adresem strony

W większej aplikacji nie chcesz ręcznie obsługiwać każdego przycisku Wstecz. Shell opisuje mapę ekranów, a szczegóły otwierasz po zarejestrowanej trasie.

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

Przejście po nazwie trasy

Przejdź do SecondPage z parametrem Imie i wróć przez ... Potem odznacz opcję rejestracji trasy i spróbuj przejść ponownie, aby zobaczyć błąd.

Najpierw przewidź, potem sprawdź: W modelu Shell przejdź do szczegółów i wróć. Porównaj adres trasy z obiektem strony w NavigationPage. Nie łącz w jednym korzeniu Shell i NavigationPage.

Ta interakcja wymaga włączonego JavaScriptu.

Shell przechodzi po nazwie, a nazwę trzeba wcześniej zarejestrować. Powrót to GoToAsync(".."), a nie PopAsync. 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
AppShell.xamlWygląd strony lub kontrolki
AppShell.xaml — wariant zamiennyFragment w istniejącym pliku
AppShell.xaml.csLogika i obsługa danych
MainPage.xaml.csLogika i obsługa danych
SecondPage.xaml.csLogika i obsługa danych
SzczegolyPage.xaml.csLogika i obsługa danych
KontaktPage.xamlWygląd strony lub kontrolki
MainPage.xamlWygląd strony lub kontrolki
SecondPage.xamlWygląd strony lub kontrolki
SzczegolyPage.xamlWygląd strony lub kontrolki
App.xaml.csLogika i obsługa danych

Zanim uruchomisz: trzy połączenia

x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja 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="MojaAplikacja.MainPage">
    <VerticalStackLayout Padding="24" Spacing="14">
        <Label Text="Start aplikacji Shell" FontSize="24" />
        <Label Text="Najpierw ekran, potem mapa przejść." />
    </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.

Zacznijmy od sklepu

Adres zamiast ręcznego tworzenia stron

Aplikacja sklepu z dolnymi zakładkami „Produkty”, „Koszyk” i „Profil” to dokładnie to, co Shell daje „z pudełka”. Kliknięcie produktu i przejście do jego szczegółów z przekazaniem identyfikatora to typowe GoToAsync z parametrem, tak jak adres URL ze stroną internetową.

**Kiedy wybrać Shell zamiast NavigationPage?** Gdy planujesz menu boczne (Flyout) albo dolne zakładki, gdy chcesz czytelne trasy tekstowe (też do deep linków) lub gdy aplikacja ma więcej niż 2–3 ekrany. Dla bardzo prostych aplikacji NavigationPage jest równie poprawny i szybszy do napisania.

7 · Porównanie

[QueryProperty] czy IQueryAttributable

SposóbJak działaKiedy
[QueryProperty]Atrybut nad klasą wstrzykuje parametr do właściwości.Jeden lub dwa proste parametry.
IQueryAttributableMetoda ApplyQueryAttributes czyta słownik.Kilka parametrów lub cały obiekt.
Słownik parametrów działa w ramach jednej sesji aplikacji. Do trwałego zapisu stanu służą `Preferences` i baza danych.

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

AppShell.xaml

Wariant A: podmień AppShell.xaml. TabBar tworzy dolne zakładki i wyłącza boczne menu Shell. Strony lokalne wskazuje ContentTemplate.

Wariant A: dolne zakładki. TabBar zawiera dwa Tab, a w nich ShellContent wskazujące strony przez DataTemplate. Dodaj stronę KontaktPage pokazaną niżej. TabBar wyłącza menu flyout: nie łącz obu wariantów bez zmiany struktury. Nie rejestruj ponownie globalnie tras tych samych stron, które są już w hierarchii.

AppShell.xamlxaml
<?xml version="1.0" encoding="utf-8" ?>
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:MojaAplikacja"
       x:Class="MojaAplikacja.AppShell">
    <TabBar Route="glowne">
        <Tab Title="Start" Route="start">
            <ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="MainPage" />
        </Tab>
        <Tab Title="Kontakt" Route="kontakt">
            <ShellContent ContentTemplate="{DataTemplate local:KontaktPage}" Route="KontaktPage" />
        </Tab>
    </TabBar>
</Shell>
  1. <Shell ... x:Class="...AppShell"> Korzeń aplikacji: zamiast NavigationPage strukturę opisuje Shell.
  2. <ShellContent ContentTemplate="{DataTemplate local:MainPage}" /> Strona startowa. DataTemplate tworzy ją dopiero w chwili potrzeby.

Budowa krok po kroku

AppShell.xaml.cs

Zarejestruj trasy w konstruktorze po InitializeComponent. Nazwa trasy jest kluczem, a typeof wskazuje klasę, która ma zostać otwarta.

Każdą stronę poza startową rejestrujesz pod unikalną nazwą, zwykle przez nameof.

AppShell.xaml.cscsharp
namespace MojaAplikacja;

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        // Rejestracja trasy: od teraz do SecondPage można przejść po nazwie
        Routing.RegisterRoute(nameof(SecondPage), typeof(SecondPage));
        Routing.RegisterRoute(nameof(SzczegolyPage), typeof(SzczegolyPage));
    }
}
  1. Routing.RegisterRoute(nameof(SecondPage), typeof(SecondPage)) Od tej chwili możesz przejść do SecondPage po nazwie z każdego miejsca aplikacji.
  2. nameof(SecondPage) Nazwa trasy wynika z nazwy klasy, więc literówka zostanie wykryta przez kompilator.

Budowa krok po kroku

Widok MainPage

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.

To widok do wcześniej omówionego pliku C# tej strony. Dodaj stronę przez szablon ContentPage (XAML), potem podmień obie części.

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="MojaAplikacja.MainPage" Title="Przykład">
    <VerticalStackLayout Padding="20" Spacing="12">
        <Entry x:Name="PoleImie" Placeholder="Imię" />
        <Button Text="Otwórz drugą stronę" Clicked="OnGoClicked" />
        <Button Text="Szczegóły produktu" Clicked="OnDetailsClicked" />
    </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

Otwórz MainPage.xaml.cs. Podmień wskazany plik po utworzeniu typów, do których się odwołuje. Objaśnienia pod kodem pokazują, które wartości przechodzą do następnego etapu.

Parametry przekazujesz w słowniku, tak jak w adresie URL strony.

MainPage.xaml.cscsharp
namespace MojaAplikacja;

public partial class MainPage : ContentPage
{
    public MainPage() => InitializeComponent();

    private async void OnGoClicked(object sender, EventArgs e)
    {
        var imie = PoleImie.Text ?? "";

        // Przejście po nazwie trasy, z parametrami w słowniku
        await Shell.Current.GoToAsync(nameof(SecondPage),
            new Dictionary<string, object> { ["Imie"] = imie });
    }
    private async void OnDetailsClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync(nameof(SzczegolyPage),
            new Dictionary<string, object> { ["Nazwa"] = "Notes", ["Cena"] = 12.50m });
    }
}
  1. Shell.Current.GoToAsync(nameof(SecondPage), słownik) Przechodzi po nazwie trasy i dołącza dane.
  2. new Dictionary<string, object> { ["Imie"] = imie } Klucz "Imie" ma odpowiadać nazwie parametru po stronie docelowej.
  3. Shell.Current Bieżąca instancja Shell, dostępna z dowolnej strony.

Budowa krok po kroku

Strona użyta przez drugą zakładkę

Dodaj lub otwórz KontaktPage.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.

Dodaj przez Dodaj → Nowy element → ContentPage (XAML). Zachowaj wygenerowany konstruktor wywołujący InitializeComponent().

KontaktPage.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="MojaAplikacja.KontaktPage" Title="Kontakt">
    <VerticalStackLayout Padding="20" Spacing="12">
        <Label Text="Kontakt do autora aplikacji" />
        <Label Text="autor@example.com" />
    </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

Widok SecondPage

Dodaj lub otwórz SecondPage.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.

To widok do wcześniej omówionego pliku C# tej strony. Dodaj stronę przez szablon ContentPage (XAML), potem podmień obie części.

SecondPage.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="MojaAplikacja.SecondPage" Title="Przykład">
    <VerticalStackLayout Padding="20" Spacing="12">
        <Label Text="{Binding Imie}" />
        <Button Text="Wróć" Clicked="OnBackClicked" />
    </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

SecondPage.xaml.cs

Otwórz SecondPage.xaml.cs. Podmień wskazany plik po utworzeniu typów, do których się odwołuje. Objaśnienia pod kodem pokazują, które wartości przechodzą do następnego etapu.

Atrybut nad klasą mówi Shellowi: „parametr Imie wstrzyknij do właściwości Imie”. To najprostszy sposób dla pojedynczych parametrów.

SecondPage.xaml.cscsharp
namespace MojaAplikacja;

// Shell wstrzyknie parametr „Imie” do właściwości Imie
[QueryProperty(nameof(Imie), "Imie")]
public partial class SecondPage : ContentPage
{
    private string _imie = "";

    public string Imie
    {
        get => _imie;
        set
        {
            _imie = value;
            OnPropertyChanged();   // powiadamia wiązanie, że wartość się zmieniła
        }
    }

    public SecondPage()
    {
        InitializeComponent();
        BindingContext = this;
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync("..");
    }
}
  1. [QueryProperty(nameof(Imie), "Imie")] Pierwszy argument to nazwa właściwości, drugi to klucz parametru.
  2. OnPropertyChanged() w setterze Bez niego wiązanie {Binding Imie} nie odświeży się, mimo że wartość się zmieniła.
  3. BindingContext = this Strona sama jest źródłem danych dla wiązań w XAML.
  4. GoToAsync("..") Powrót o jeden poziom w górę.

Budowa krok po kroku

Widok SzczegolyPage

Dodaj lub otwórz SzczegolyPage.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.

To widok do wcześniej omówionego pliku C# tej strony. Dodaj stronę przez szablon ContentPage (XAML), potem podmień obie części.

SzczegolyPage.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="MojaAplikacja.SzczegolyPage" Title="Przykład">
    <VerticalStackLayout Padding="20" Spacing="12">
        <Label x:Name="EtykietaNazwa" />
        <Label x:Name="EtykietaCena" />
        <Button Text="Wróć" Clicked="OnBackClicked" />
    </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

SzczegolyPage.xaml.cs

Parametry nawigacji odczytuje strona docelowa. QueryProperty ma ograniczenia przy pełnym trimming/NativeAOT — dla takich konfiguracji użyj IQueryAttributable, jak zaznaczono w materiale.

Wygodniejsze, gdy parametrów jest kilka albo przekazujesz cały obiekt: odczytujesz wszystkie naraz w jednej metodzie.

SzczegolyPage.xaml.cscsharp
namespace MojaAplikacja;

// Drugi sposób odbioru parametrów: interfejs IQueryAttributable
public partial class SzczegolyPage : ContentPage, IQueryAttributable
{
    public SzczegolyPage()
    {
        InitializeComponent();
    }

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        // Wszystkie parametry odczytujemy naraz ze słownika
        if (query.TryGetValue("Nazwa", out var nazwa))
            EtykietaNazwa.Text = nazwa?.ToString();

        if (query.TryGetValue("Cena", out var cena))
            EtykietaCena.Text = $"Cena: {cena} zł";
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync("..");
    }
}
  1. IQueryAttributable Interfejs z jedną metodą ApplyQueryAttributes.
  2. query.TryGetValue("Nazwa", out var nazwa) Bezpieczny odczyt: nie zgłasza błędu, gdy parametru brakuje.

Budowa krok po kroku

Uruchom Shell w App.xaml.cs

Okno tworzy bezpośrednio AppShell. Nie opakowuj Shell w NavigationPage.

W tej lekcji startem jest AppShell. Nie dodawaj NavigationPage z poprzedniej lekcji: to dwa różne warianty nawigacji.

App.xaml.cscsharp
namespace MojaAplikacja;
public partial class App : Application
{
    public App() => InitializeComponent();
    protected override Window CreateWindow(IActivationState? activationState)
        => new Window(new AppShell());
}

Zatrzymaj się przed kolejnym wariantem

Nie zastępuj jeszcze działającej wersji. Wykonaj tę próbę i dopiero potem przejdź do dodatku.

Dopiero teraz: drugi wariant lub rozszerzenie

To następny etap tej lekcji, nie warunek pierwszego uruchomienia. Porównaj jego rolę z działającą bazą. Jeśli brakuje czasu, wróć do niego po sprawdzeniu podstawowej wersji.

Dodatek do działającej wersji

Wariant B zamiast TabBar: menu boczne

Wariant B wykonaj dopiero po działającym A. ZASTĄP cały AppShell.xaml, nie doklejaj drugiego Shell. FlyoutItem zawiera własne zakładki.

Podmień AppShell.xaml wariantem B, żeby porównać menu boczne oraz zakładki wewnątrz FlyoutItem. Nie doklejaj drugiego Shell do tego samego pliku. Oba warianty korzystają z tego samego AppShell.xaml.cs.

AppShell.xaml — wariant zamiennycsharp
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:MojaAplikacja"
       x:Class="MojaAplikacja.AppShell" FlyoutBehavior="Flyout">
    <FlyoutItem Title="Moje strony" Route="strony">
        <Tab Title="Start" Route="start">
            <ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="MainPage" />
        </Tab>
        <Tab Title="Kontakt" Route="kontakt">
            <ShellContent ContentTemplate="{DataTemplate local:KontaktPage}" Route="KontaktPage" />
        </Tab>
    </FlyoutItem>
</Shell>

Plik .xaml.cs też musi istnieć

Szablon strony lub ContentView generuje poniższe klasy. Nie dodawaj ich drugi raz, jeśli Visual Studio już je utworzyło. W każdej parze musi pozostać InitializeComponent().

KontaktPage — minimalny kod za widokiem

KontaktPage.xaml.cscsharp
namespace MojaAplikacja;

public partial class KontaktPage : ContentPage
{
    public KontaktPage() => InitializeComponent();
}
  1. partial Druga część klasy powstaje z XAML. Nie twórz ręcznie pól kontrolek ani drugiej metody InitializeComponent.

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.

8 · Czego unikać

Cztery błędy z Shell

Brak `Routing.RegisterRoute`
Tak nie
await Shell.Current.GoToAsync(nameof(SecondPage));   // trasa niezarejestrowana

Wyjątek „nie znaleziono trasy”.

Tak
Routing.RegisterRoute(nameof(SecondPage), typeof(SecondPage));   // w AppShell

Shell przechodzi tylko do zarejestrowanych tras.

Brak `OnPropertyChanged` w setterze
Tak nie
public string Imie { get; set; }

Wiązanie {Binding Imie} nie odświeża się po ustawieniu parametru.

Tak
set { _imie = value; OnPropertyChanged(); }

Wiązanie musi dostać powiadomienie o zmianie.

Mieszanie `GoToAsync("..")` z `PopAsync`
Tak nie
await Navigation.PopAsync();   // w aplikacji z Shell

Dwa różne systemy nawigacji w jednej aplikacji prowadzą do niespójnego zachowania.

Tak
await Shell.Current.GoToAsync("..");

W aplikacji z Shell konsekwentnie używaj GoToAsync.

Słownik parametrów jako trwały zapis
Tak nie
Przekazuję przez słownik dane, które mają przetrwać zamknięcie aplikacji.

Po zamknięciu aplikacji dane znikają.

Tak
Preferences albo baza danych

Parametry trasy żyją tylko w jednej sesji.

Ćwiczenie z pomocą · samodzielna praca

6. Mała zmiana, którą zrobisz samodzielnie

Dodaj trasę do własnej strony PomocPage.

Oczekiwany rezultat
Dodaj trasę do własnej strony PomocPage.
Dotychczasowe działanie ma pozostać poprawne.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

Dodaj ContentPage (XAML) o nazwie PomocPage.

Krok 2 — pokaż podpowiedź

W konstruktorze AppShell zarejestruj unikalną trasę Routing.RegisterRoute.

Krok 3 — pokaż podpowiedź

Wywołaj GoToAsync z dokładnie tą nazwą trasy; literówka nie tworzy nowej strony.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

9 · Sprawdź się

Trasa, parametr czy odbiór?

Cztery sytuacje.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Shell przechodzi po nazwie trasy: Routing.RegisterRoute w AppShell, potem Shell.Current.GoToAsync(nameof(Strona)).
  2. Powrót to GoToAsync(".."). Nie mieszaj go z PopAsync.
  3. Parametry przekazujesz słownikiem w GoToAsync.
  4. Odbiór: [QueryProperty] (proste) albo IQueryAttributable (kilka parametrów).
  5. Setter właściwości woła OnPropertyChanged(), aby wiązanie się odświeżyło.

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