Wprowadzenie, slajd 1/3
MIT APP Inventor 2 - Zadania samodzielne

Programowanie aplikacji mobilnych - zadania krok po kroku

  • Środowisko: MIT App Inventor (ai2.appinventor.mit.edu), testowanie na smartfonie z Androidem przez AI Companion lub plik .apk.
  • Materiał bazowy: prezentacje 1-5 z pam.netstudio.net.pl oraz ćwiczenie ex1 (Kalkulator).
  • Forma: każde zadanie rozpisane na slajdy etapowe: specyfikacja, interfejs w Designerze, bloki w Blocks, testy na telefonie, rozszerzenia.
  • Idea: student wykonuje zadania samodzielnie, krok po kroku, sprawdzając po każdym etapie, czy aplikacja się uruchamia.
Wprowadzenie, slajd 2/3
Spis treści - 10 zadań

Plan arkusza zadań

Zasada ogólna: po każdym etapie aplikacja powinna się uruchamiać - nawet jeśli nie wszystkie elementy jeszcze działają. Lepiej mieć częste, małe sukcesy niż jeden wielki skok na końcu.
Wprowadzenie, slajd 3/3
Cele uczenia się arkusza zadań

Co student będzie umiał po wykonaniu wszystkich zadań

  • W1: samodzielnie zaprojektować interfejs aplikacji w oknie Designer - dobrać komponenty, ustawić właściwości, nazwać elementy czytelnie.
  • W2: programować w oknie Blocks z wykorzystaniem zdarzeń, zmiennych, instrukcji warunkowych, procedur i bloków matematycznych.
  • W3: stosować walidację danych wejściowych i obsługiwać przypadki brzegowe (puste pola, dzielenie przez zero, brak danych GPS).
  • W4: korzystać z komponentów czujników (akcelerometr), multimediów (aparat, syntezator mowy, rozpoznawanie mowy) i danych trwałych (TinyDB).
  • W5: budować aplikacje wieloekranowe z nawigacją open another screen / close screen i przekazywaniem danych między ekranami.
  • W6: testować aplikacje na prawdziwym smartfonie przez AI Companion oraz budować plik instalacyjny .apk.
  • K1: samodzielnie rozwinąć którekolwiek zadanie o własną funkcję i uzasadnić wprowadzone zmiany.
Po wykonaniu dziesięciu zadań student dysponuje kompletnym warsztatem: od powitania Hello World po wieloekranowy trener z rankingiem i danymi trwałymi.
Zadanie 1, slajd 1/6
Zadanie 1: Spersonalizowane powitanie - cel
Cel zadania: wprowadzenie do zadania 1 - rozbudowa przykładu Powitanie o pole tekstowe, blok join i walidację.

Aplikacja Powitanie+ (Hello World v2)

  • Cel aplikacji: użytkownik wpisuje swoje imię, naciska przycisk, a aplikacja wyświetla spersonalizowane powitanie i zmienia kolor tekstu.
  • Poziom: łatwy - pierwsze samodzielne zadanie, utrwalenie podstaw z prezentacji 3.
  • Nowe pojęcia: blok join (łączenie tekstów), odczyt właściwości Text z pola tekstowego, zmiana TextColor z bloków, walidacja pustego pola przez Notifier.
  • Wymagania:
    • Po wpisaniu imienia i kliknięciu przycisku pojawia się napis: „Witaj, [imię]! Miło Cię widzieć."
    • Kolor tekstu powitania zmienia się na zielony.
    • Jeśli pole jest puste, aplikacja pokazuje komunikat ostrzegawczy zamiast powitania.
    • Drugi przycisk „Wyczysc" przywraca stan początkowy.
  • Komponenty: Screen1, Label1 (podpowiedź), TextBox1, Button1 (Powitaj), Label1 (powitanie), Button2 (Wyczyść), Notifier1.
  • Prezentacja bazowa: prezentacja 3, slajdy 5-11 (Przykład 1: Powitanie).
Zadanie 1, slajd 2/6
Zadanie 1: nowy projekt i właściwości Screen1

Etap 1: utworzenie projektu

  1. Otwórz przeglądarkę i wejdź na adres ai2.appinventor.mit.edu.
  2. Zaloguj się przez konto Google (przycisk Sign in / Zaloguj się).
  3. W menu górnym wybierz Projects → Start new project.
  4. Wpisz nazwę projektu: powitanie_plus (bez spacji i polskich znaków - tylko litery łacińskie, cyfry i podkreślnik).
  5. Potwierdź przyciskiem OK - otworzy się okno Designer z pustym ekranem Screen1.

Etap 1b: właściwości ekranu Screen1

  • Kliknij Screen1 w panelu Components (po prawej stronie).
  • W panelu Properties ustaw:
    • Title: Powitanie+ - to będzie tytuł na pasku telefonu.
    • AppName: Powitanie+ - nazwa widoczna po instalacji.
    • AlignHorizontal: Center - wszystkie komponenty wyśrodkowane poziomo.
    • BackgroundColor: Light Gray lub własny kod #F0F0F0.
    • ScreenOrientation: Portrait - ekran nie obraca się.
Wskazówka: Title i AppName ustaw raz tutaj i nie wracaj do nich przy każdym teście. Nazwa projektu musi być bez polskich znaków - zasada dotyczy wszystkich projektów w kursie.
Zadanie 1, slajd 3/6
Zadanie 1: budowa interfejsu w Designerze

Etap 2: komponenty widoczne na ekranie

  1. Z palety User Interface przeciągnij na ekran Label - zmień jego nazwę na lblPodpowiedz (kliknij prawym w Components → Rename). Ustaw:
    • Text: Wpisz swoje imię:
    • FontSize: 18
    • Width: Fill parent
  2. Przeciągnij TextBox - zmień nazwę na txtImie. Ustaw:
    • Hint: np. Anna (podpowiedź widoczna w pustym polu)
    • FontSize: 22
    • Width: Fill parent
  3. Przeciągnij Button - zmień nazwę na btnPowitaj. Ustaw:
    • Text: Powitaj mnie
    • BackgroundColor: Blue
    • TextColor: White
    • FontSize: 20
    • Width: Fill parent
    • Height: 50
  4. Przeciągnij drugi Button - zmień nazwę na btnWyczysc. Ustaw Text = Wyczyść, BackgroundColor = Gray, Width = Fill parent.
  5. Przeciągnij Label - zmień nazwę na lblPowitanie. Ustaw:
    • Text: (puste - usuń wszystko)
    • FontSize: 26
    • Width: Fill parent
    • Height: 80
    • TextAlignment: Center
  6. Z palety User Interface przeciągnij Notifier - to komponent niewidoczny na ekranie, ale widoczny w drzewie Components. Zmień nazwę na Notifier1.
Zasada: zmień nazwy komponentów TERAZ, przed napisaniem bloków. Zmiana nazw po napisaniu bloków wymaga ręcznej aktualizacji każdego odwołania - w Blocks trwa to znacznie dłużej niż w Designerze.
Zadanie 1, slajd 4/6
Zadanie 1: zmienne i logika główna w Blocks

Etap 3: pierwsze bloki w oknie Blocks

Kliknij przycisk Blocks w prawym górnym rogu okna (obok Designer).

Blok 1: zdarzenie kliknięcia przycisku Powitaj

  1. W panelu bloków po lewej kliknij btnPowitaj.
  2. Przeciągnij na pole robocze brązowy blok when btnPowitaj.Click do.
  3. Z listy lblPowitanie weź blok set lblPowitanie.Text to i wpnij go wewnątrz zdarzenia Click.
  4. W miejsce wartości wstaw blok join z sekcji Text (szara kategoria po lewej).
  5. Przeciągnij dymek z napisem "" (pusty napis) i wpnij do pierwszego slotu join. Zmień tekst na: Witaj, (z konieczną spacją po przecinku!).
  6. Z listy txtImie weź blok get txtImie.Text i wpnij do drugiego slotu join.
  7. Dodaj trzeci slot do join (kliknij niebieskie kółko zębate → dodaj element). Wpisz: ! Miło Cię widzieć.
Kod słownie: KIEDY kliknięto btnPowitaj, USTAW lblPowitanie.Text na połączenie: „Witaj, " + imię z pola + „! Miło Cię widzieć."

Blok 2: zmiana koloru tekstu

  1. Nadal wewnątrz zdarzenia Click dodaj blok set lblPowitanie.TextColor to z listy lblPowitanie.
  2. W miejsce koloru wpnij blok Green z sekcji Colors.
Zadanie 1, slajd 5/6
Zadanie 1: walidacja pustego pola i przycisk Wyczyść

Etap 4: obsługa błędów - gdy pole jest puste

  1. Z sekcji Control weź blok if...then...else i wpnij go NA GÓRĘ zawartości zdarzenia Click (przed blokiem set Text).
  2. W warunek wpnij porównanie z sekcji Logic: =
  3. Z lewej strony porównania wpnij get txtImie.Text, z prawej strony napis "" (pusty napis - dymek z Text).
  4. W gałęzi then (gdy pole puste) wpnij: call Notifier1.ShowAlert z listy Notifier1. W argument wpisz napis: Wpisz swoje imię, proszę!
  5. Całą dotychczasową zawartość (join + TextColor) przenieś do gałęzi else (gdy pole NIE jest puste).
Kod słownie: JESZCZE ZANIM wyświetlę powitanie, SPRAWDŹ czy pole nie jest puste. JEŚLI jest puste → pokaż alert. W PRZECIWNYM RAZIE → zbuduj powitanie i zmień kolor.

Etap 5: przycisk Wyczyść

  1. Weź blok when btnWyczysc.Click do.
  2. Wewnątrz wpnij: set lblPowitanie.Text to "" (pusty napis).
  3. Dodaj: set lblPowitanie.TextColor to Dark Gray (kolor z sekcji Colors).
  4. Dodaj: set txtImie.Text to "" (wyczyść też pole tekstowe).
Zadanie 1, slajd 6/6
Zadanie 1: testowanie na telefonie i lista kontrolna

Etap 6: połączenie z telefonem i testy

  1. Na smartfonie zainstaluj aplikację MIT AI2 Companion (z Google Play).
  2. Upewnij się, że telefon i komputer są w tej samej sieci Wi-Fi.
  3. W App Inventor wybierz menu Connect → AI Companion.
  4. Na ekranie pojawi się kod QR - zeskanuj go w aplikacji Companion na telefonie (przycisk Scan QR) lub wpisz 6-znakowy kod ręcznie.
  5. Poczekaj, aż aplikacja załaduje się na telefonie (może to potrwać kilka sekund).

Scenariusze testowe do sprawdzenia

Test 1: Wpisz imię „Anna", kliknij „Powitaj mnie". Oczekiwany wynik: napis „Witaj, Anna! Miło Cię widzieć." na zielono.
Test 2: Wyczyść pole (usuń imię ręcznie), kliknij „Powitaj". Oczekiwany wynik: alert „Wpisz swoje imię, proszę!", powitanie NIE pojawia się.
Test 3: Kliknij „Wyczyść". Oczekiwany wynik: powitanie znika, pole jest puste, tekst wraca do szarości.
Test 4: Wpisz długie imię (np. „Aleksandra Katarzyna"), kliknij „Powitaj". Sprawdź, czy tekst mieści się na ekranie.
Jeśli zmiany nie widać na telefonie: zmień dowolną właściwość w Designerze (np. kolor Screen1) - to wymusi odświeżenie widoku na urządzeniu. Jeśli to nie pomoże, sprawdź połączenie Wi-Fi i zrestartuj Companion.

Lista kontrolna - co musi działać

  • ✓ Wpisanie imienia i kliknięcie daje spersonalizowany napis
  • ✓ Puste pole → komunikat ostrzegawczy (aplikacja nie buduje pustego powitania)
  • ✓ Kolor tekstu zmienia się na zielony po powitaniu
  • ✓ Przycisk Wyczyść przywraca stan początkowy
  • ✓ Brak błędów/crashy na telefonie
Zadanie 2, slajd 1/7
ZADANIE 2: Kalkulator czterech działań - cel
Cel zadania: wprowadzenie do zadania 2 - kalkulator z ListPickerem, zmienną globalną, instrukcją if/else i walidacją danych wejściowych.

Aplikacja Kalkulator v1

  • Cel aplikacji: podanie dwóch liczb i wybór działania z listy, a następnie wyświetlenie wyniku. Obsługa czterech działań: dodawanie, odejmowanie, mnożenie, dzielenie.
  • Poziom: średni - pierwsze zadanie z ListPickerem i wielokrotnym warunkiem.
  • Nowe pojęcia: zmienna globalna, ListPicker (lista wyboru), zdarzenie AfterPicking, właściwość Selection, instrukcja if/else if/else, Notifier.ShowAlert.
  • Wymagania:
    • Dwa pola liczbowe (NumbersOnly) i lista wyboru działania.
    • Przycisk Oblicz uruchamia właściwe działanie na podstawie wyboru z listy.
    • Puste pole → komunikat „Wpisz obie liczby!".
    • Brak wyboru działania → komunikat „Wybierz działanie z listy!".
    • Dzielenie przez zero → komunikat „Nie można dzielić przez zero!".
    • Przycisk Wyczyść resetuje pola i wynik.
  • Komponenty: 2× Label, 2× TextBox (NumbersOnly), ListPicker, Button (Oblicz), Button (Wyczyść), Label (Wynik), Notifier.
  • Prezentacja bazowa: prezentacja 3, slajdy 12-18; ćwiczenie ex1 (Kalkulator).
Zadanie 2, slajd 2/7
Zadanie 2: nowy projekt i pola liczbowe

Etap 1: utworzenie projektu i ustawienia Screen1

  1. Menu Projects → Start new project, nazwa: kalkulator_v1.
  2. Ustawienia Screen1: Title = Kalkulator, AlignHorizontal = Center, BackgroundColor = White, ScreenOrientation = Portrait.

Etap 2: pola do wprowadzania liczb

  1. Dodaj Label → Rename na lblLiczba1:
    • Text = Liczba 1:, FontSize = 16, Width = Fill parent, TextAlignment = Left
  2. Dodaj TextBox → Rename na txtLiczba1:
    • NumbersOnly = true (klawiatura pokazuje tylko cyfry), FontSize = 22, Width = Fill parent, Hint = wpisz liczbę
  3. Dodaj Label → Rename na lblLiczba2:
    • Text = Liczba 2:, FontSize = 16, Width = Fill parent
  4. Dodaj TextBox → Rename na txtLiczba2:
    • NumbersOnly = true, FontSize = 22, Width = Fill parent, Hint = wpisz liczbę
NumbersOnly ogranicza klawiaturę do danych liczbowych, ale NIE gwarantuje znaku minus ani separatora dziesiętnego (zależy od urządzenia). To ułatwia pracę, ale nie zastępuje walidacji danych - puste pole nadal trzeba sprawdzić warunkiem.
Zadanie 2, slajd 3/7
Zadanie 2: lista wyboru, przyciski i etykieta wyniku

Etap 3: ListPicker - lista wyboru działania

  1. Z palety User Interface przeciągnij ListPicker → Rename na lstDzialanie.
  2. Ustaw jego właściwości:
    • Text: Wybierz działanie (tekst widoczny na zamkniętej liście)
    • Elements: wpisz: dodaj, odejmij, pomnóż, podziel (cztery pozycje rozdzielone przecinkami, BEZ spacji po przecinkach)
    • Width: Fill parent
    • BackgroundColor: Light Blue

Etap 4: przyciski i etykieta wyniku

  1. Dodaj Button → Rename na btnOblicz:
    • Text = Oblicz, BackgroundColor = Orange, FontSize = 22, Width = Fill parent, Height = 55, TextColor = White
  2. Dodaj Button → Rename na btnWyczysc:
    • Text = Wyczyść, BackgroundColor = Gray, Width = Fill parent
  3. Dodaj Label → Rename na lblWynik:
    • Text = Wynik: –, FontSize = 32, TextColor = Blue, Width = Fill parent, TextAlignment = Center, Height = 70
  4. Z palety User Interface przeciągnij Notifier (niewidoczny) → Rename na Notifier1.
Właściwość Elements listy ListPicker to lista pozycji oddzielonych przecinkami. Każda pozycja staje się wartością, którą odczytamy w blokach przez właściwość Selection. Kolejność na liście nie ma znaczenia dla logiki - ważne jest dokładne dopasowanie tekstu w warunkach.
Zadanie 2, slajd 4/7
Zadanie 2: zmienna globalna i obsługa ListPicker

Etap 5: zmienne globalne w oknie Blocks

  1. Przejdź do okna Blocks.
  2. Z sekcji Variables (Built-in, kolor ciemnozielony) przeciągnij blok initialize global ... to ....
  3. Nazwij zmienną: wybraneDzialanie, wartość początkową ustaw na pusty napis "".
  4. Dla czytelności dodaj komentarz: kliknij prawym przyciskiem na puste miejsce → Add Comment → wpisz: „Przechowuje wybór użytkownika z listy działań".

Etap 6: zapis wyboru z ListPicker

  1. Z listy lstDzialanie weź blok when lstDzialanie.AfterPicking do (zdarzenie uruchamiane po zamknięciu listy i wybraniu pozycji).
  2. Wewnątrz wpnij blok set global wybraneDzialanie to (z sekcji Variables).
  3. Jako wartość wpnij blok get lstDzialanie.Selection (z listy lstDzialanie - Selection przechowuje ostatnio wybraną pozycję).
Kod słownie: KIEDY użytkownik wybrał pozycję z listy, USTAW zmienną wybraneDzialanie na wartość Selection (tekst wybranej pozycji: „dodaj", „odejmij", „pomnóż" lub „podziel").

Etap 7: wyświetlanie wyboru w etykiecie (opcjonalnie)

  1. Dodaj do Designer Label lblWybrane z Text = „Wybrane: –".
  2. W tym samym zdarzeniu AfterPicking dodaj: set lblWybrane.Text to join("Wybrane: ", get global wybraneDzialanie).
Zadanie 2, slajd 5/7
Zadanie 2: przycisk Oblicz - walidacja wejścia

Etap 8: struktura zdarzenia btnOblicz.Click - część walidacyjna

  1. Weź blok when btnOblicz.Click do.
  2. Z sekcji Control weź if...then...else if...then...else. Rozwiń koło zębate i dodaj TRZY gałęzie else if (razem: if + 3× else if + else).
  3. Gałąź 1 (puste pola):
    • Warunek: get txtLiczba1.Text = "" or get txtLiczba2.Text = ""
    • (blok or z Logic, porównania = z Logic lub Math)
    • then: call Notifier1.ShowAlert → napis: Wpisz obie liczby!
  4. Gałąź 2 (brak wyboru działania):
    • else if: get global wybraneDzialanie = ""
    • then: call Notifier1.ShowAlert → napis: Wybierz działanie z listy!
  5. Gałąź 3 (dzielenie przez zero):
    • else if: get global wybraneDzialanie = "podziel" and get txtLiczba2.Text = "0"
    • (blok and z Logic)
    • then: call Notifier1.ShowAlert → napis: Nie można dzielić przez zero!
  6. Gałąź else (właściwe obliczenia): tu trafi logika czterech działań (slajd 15).
Kolejność warunków ma znaczenie: najpierw najprostsze sprawdzenia (puste pola), potem bardziej specyficzne (dzielenie przez zero), na końcu właściwa logika. Gdyby odwrócić kolejność, komunikaty mogłyby być mylące.
Zadanie 2, slajd 6/7
Zadanie 2: logika czterech działań

Etap 9: gałęzie właściwych obliczeń (w gałęzi else)

W gałęzi else (gdy walidacja przeszła pomyślnie) dodaj cztery zagnieżdżone warunki - jeden na każde działanie:

  1. Dodawanie:
    • if get global wybraneDzialanie = "dodaj" then
    • set lblWynik.Text to get txtLiczba1.Text + get txtLiczba2.Text
    • (blok + z sekcji Math)
  2. Odejmowanie (else if):
    • else if ... = "odejmij" then
    • set lblWynik.Text to get txtLiczba1.Text - get txtLiczba2.Text
  3. Mnożenie (else if):
    • else if ... = "pomnóż" then
    • set lblWynik.Text to get txtLiczba1.Text * get txtLiczba2.Text
  4. Dzielenie (else):
    • else (bo jeśli nie dodaj/odejmij/pomnóż, to na pewno podziel - dzielenie przez zero już odfiltrowane)
    • set lblWynik.Text to get txtLiczba1.Text / get txtLiczba2.Text
Przykład działania: txtLiczba1 = 12, txtLiczba2 = 4, wybraneDzialanie = „pomnóż" → gałąź mnożenia ustawia lblWynik.Text na 48.

Etap 10: przycisk Wyczyść

  1. Weź when btnWyczysc.Click do.
  2. Wewnątrz: set txtLiczba1.Text to "", set txtLiczba2.Text to "", set lblWynik.Text to "Wynik: –", set global wybraneDzialanie to "".
Zadanie 2, slajd 7/7
Zadanie 2: testowanie na telefonie

Etap 11: połączenie z telefonem

  1. Connect → AI Companion → zeskanuj kod QR na telefonie.
  2. Upewnij się, że oba urządzenia są w tej samej sieci Wi-Fi.

Scenariusze testowe

Test 1 (dodawanie): Wpisz 12 i 30, wybierz „dodaj", kliknij Oblicz → oczekiwany wynik: 42.
Test 2 (odejmowanie): Wpisz 50 i 8, wybierz „odejmij" → wynik: 42.
Test 3 (mnożenie): Wpisz 6 i 7, wybierz „pomnóż" → wynik: 42.
Test 4 (dzielenie): Wpisz 84 i 2, wybierz „podziel" → wynik: 42.
Test 5 (puste pola): Wyczyść pola, wybierz „dodaj", kliknij Oblicz → alert „Wpisz obie liczby!".
Test 6 (brak wyboru): Wpisz liczby, NIE wybieraj z listy, kliknij Oblicz → alert „Wybierz działanie z listy!".
Test 7 (dzielenie przez zero): Wpisz 10 i 0, wybierz „podziel" → alert „Nie można dzielić przez zero!".
Test 8 (reset): Kliknij Wyczyść → pola puste, wynik = „Wynik: –".

Lista kontrolna

  • ✓ Wszystkie cztery działania liczą poprawnie
  • ✓ Puste pole → komunikat (aplikacja nie crashuje)
  • ✓ Brak wyboru → komunikat
  • ✓ Dzielenie przez zero → komunikat (brak Infinity/NaN)
  • ✓ Po Wyczyść można zacząć od nowa bez restartu
Zadanie 3, slajd 1/7
ZADANIE 3: Quiz z punktacją - cel
Cel zadania: wprowadzenie do zadania 3 - quiz pięciu pytań z listami, indeksowaniem, punktacją i ekranem końcowym.

Aplikacja Quiz wiedzy

  • Cel aplikacji: wyświetlanie kolejno 5 pytań wielokrotnego wyboru (3 odpowiedzi: A, B, C). Użytkownik wybiera odpowiedź przyciskiem, program ocenia i na końcu pokazuje wynik z punktacją.
  • Poziom: średni - pierwsze zadanie z listami i indeksowaniem.
  • Nowe pojęcia: listy (initialize global ... to list), blok select list item, zmienna licznikowa, zwiększanie licznika o 1, blok join z liczbami, blok compare texts (porównanie napisów).
  • Wymagania:
    • 5 pytań z 3 odpowiedziami każda - pytania i odpowiedzi w listach.
    • Po kliknięciu odpowiedzi: komunikat „Dobrze!" (zielony) lub „Źle! Poprawna: [odpowiedź]" (czerwony).
    • Zablokowanie dalszych kliknięć po udzieleniu odpowiedzi na dane pytanie.
    • Licznik punktów aktualizowany na bieżąco („Punkty: 3/5").
    • Po 5 pytaniach - ekran końcowy z wynikiem i przyciskiem „Zagraj jeszcze raz".
  • Komponenty: Label (pytanie), 3× Button (A/B/C), Label (feedback), Label (punkty), Button (Następne), Button (Jeszcze raz - na końcu).
  • Prezentacja bazowa: prezentacja 3, slajdy 42-47 (Przykład 7: quiz).
Zadanie 3, slajd 2/7
Zadanie 3: projekt i przygotowanie pytań

Etap 1: nowy projekt i przygotowanie treści

  1. Projects → Start new project, nazwa: quiz_v1.
  2. PRZED otwarciem Designer przygotuj 5 pytań na kartce: każde pytanie ma 3 odpowiedzi (A, B, C) i jedną poprawną. Przykład:
    • Q1: Stolica Polski? → A) Kraków B) Warszawa C) Gdańsk → poprawna: B
    • Q2: Największa planeta Układu Słonecznego? → A) Saturn B) Jowisz C) Neptun → poprawna: B
    • Q3: Ile nóg ma pająk? → A) 6 B) 8 C) 10 → poprawna: B
    • Q4: Kto napisał „Pana Tadeusza"? → A) Mickiewicz B) Słowacki C) Krasiński → poprawna: A
    • Q5: Ile to 7 × 8? → A) 54 B) 56 C) 64 → poprawna: B

Etap 2: ustawienia Screen1

  1. Title = Quiz wiedzy, AlignHorizontal = Center, BackgroundColor = Light Cyan lub #E0F7FA.
Dlaczego najpierw pytania? Bez przygotowanej treści nie wiesz, ile komponentów będzie potrzebnych ani jak ułożyć interfejs. Specyfikacja poprzedza projekt - zasada obowiązuje w każdym zadaniu.
Zadanie 3, slajd 3/7
Zadanie 3: budowa interfejsu quizu

Etap 3: komponenty quizu w Designerze

  1. lblNumer (Label): Text = Pytanie 1/5, FontSize = 14, TextColor = Gray.
  2. lblPytanie (Label): Text = (puste), FontSize = 22, Width = Fill parent, Height = 90, TextAlignment = Center, BackgroundColor = White.
  3. HorizontalArrangement1 (z palety Layout): Width = Fill parent. Wewnątrz niego umieść 3 przyciski A, B, C:
btnOdpA (Button): Text = A), BackgroundColor = Blue, TextColor = White, FontSize = 20, Width = 100, Height = 60
btnOdpB (Button): Text = B), BackgroundColor = Blue, TextColor = White, FontSize = 20, Width = 100, Height = 60
btnOdpC (Button): Text = C), BackgroundColor = Blue, TextColor = White, FontSize = 20, Width = 100, Height = 60
  1. lblFeedback (Label): Text = (puste), FontSize = 20, Width = Fill parent, Height = 50, TextAlignment = Center.
  2. lblPunkty (Label): Text = Punkty: 0/5, FontSize = 18, TextColor = Dark Green, Width = Fill parent, TextAlignment = Center.
  3. btnNastepne (Button): Text = Następne pytanie →, BackgroundColor = Green, TextColor = White, FontSize = 18, Width = Fill parent, Height = 50.
  4. btnStartOdNowa (Button): Text = 🔄 Zagraj jeszcze raz, BackgroundColor = Orange, Width = Fill parent, Visible = false (na razie ukryty).
Zadanie 3, slajd 4/7
Zadanie 3: listy pytań i procedura pokazPytanie

Etap 4: listy z pytaniami w oknie Blocks

  1. Przejdź do okna Blocks.
  2. Utwórz 5 list globalnych (z sekcji Variables → blok initialize global ... to list):
initialize global pytania to list → kliknij niebieskie kółko → dodaj 5 slotów → wpisz 5 tekstów pytań
initialize global odpA to list → 5 odpowiedzi wariant A
initialize global odpB to list → 5 odpowiedzi wariant B
initialize global odpC to list → 5 odpowiedzi wariant C
initialize global poprawna to list → np. "B", "B", "B", "A", "B" (litery poprawnych odpowiedzi)
initialize global numerPytania to 1 (zmienna licznikowa - zaczynamy od pytania 1)
initialize global punkty to 0 (licznik punktów)

Etap 5: procedura pokazPytanie

  1. Z sekcji Procedures weź blok to procedure do i nazwij: pokazPytanie.
  2. Wewnątrz procedury umieść:
    • set lblPytanie.Text to select list item global pytania, get global numerPytania
    • set btnOdpA.Text to join("A) ", select list item global odpA, get global numerPytania)
    • set btnOdpB.Text to join("B) ", select list item global odpB, get global numerPytania)
    • set btnOdpC.Text to join("C) ", select list item global odpC, get global numerPytania)
    • set lblFeedback.Text to "" (wyczyść komunikat)
    • set lblNumer.Text to join("Pytanie ", get global numerPytania, "/5")
    • set btnOdpA.Enabled to true (odblokuj przyciski)
    • set btnOdpB.Enabled to true
    • set btnOdpC.Enabled to true
  3. Wywołaj procedurę w zdarzeniu when Screen1.Initialize → call pokazPytanie.
Zadanie 3, slajd 5/7
Zadanie 3: ocena odpowiedzi - trzy przyciski

Etap 6: zdarzenie btnOdpA.Click - ocena odpowiedzi A

  1. Weź when btnOdpA.Click do.
  2. Dodaj warunek:
    • if select list item global poprawna, get global numerPytania = "A" then
    • then (poprawna): set global punkty to get global punkty + 1
    • + set lblFeedback.Text to "✓ Dobrze!"
    • + set lblFeedback.TextColor to Green
    • else (błędna): set lblFeedback.Text to join("✗ Źle! Poprawna: ", select list item global poprawna, get global numerPytania)
    • + set lblFeedback.TextColor to Red
  3. Po if/else (w obu gałęziach lub poza nimi) dodaj:
    • set lblPunkty.Text to join("Punkty: ", get global punkty, "/5")
    • set btnOdpA.Enabled to false (zablokuj ten przycisk)
    • set btnOdpB.Enabled to false
    • set btnOdpC.Enabled to false

Etap 7: powtórz dla btnOdpB i btnOdpC

  1. Kliknij prawym przyciskiem na zdarzenie btnOdpA.Click → Duplicate.
  2. Przenieś kopię obok i zmień tylko: nazwę zdarzenia (btnOdpB) oraz literę w porównaniu („B") i w komunikacie błędu.
  3. Powtórz dla btnOdpC (litera „C").
Kopiowanie bloków prawym przyciskiem → Duplicate jest znacznie szybsze niż budowanie od nowa. Zmieniamy tylko to, co konieczne: nazwę zdarzenia i literę odpowiedzi.
Zadanie 3, slajd 6/7
Zadanie 3: przycisk Następne i koniec quizu

Etap 8: btnNastepne.Click - przejście do kolejnego pytania

  1. Weź when btnNastepne.Click do.
  2. Zwiększ licznik: set global numerPytania to get global numerPytania + 1.
  3. Sprawdź, czy to było ostatnie pytanie:
    • if get global numerPytania > 5 then (blok > z Math)
    • then (koniec quizu):
      • set lblPytanie.Text to join("🎉 Koniec quizu! Wynik: ", get global punkty, "/5")
      • set lblFeedback.Text to ""
      • set btnOdpA.Visible to false (ukryj przyciski odpowiedzi)
      • set btnOdpB.Visible to false
      • set btnOdpC.Visible to false
      • set btnNastepne.Visible to false
      • set btnStartOdNowa.Visible to true (pokaż przycisk restartu)
      • Opcjonalnie: ocena słowna - jeśli punkty ≥ 4 → „Świetnie!", 2-3 → „Dobrze!", 0-1 → „Ćwicz dalej!"
    • else (kolejne pytanie): call pokazPytanie

Etap 9: btnStartOdNowa.Click - restart

  1. Weź when btnStartOdNowa.Click do.
  2. Wewnątrz:
    • set global numerPytania to 1
    • set global punkty to 0
    • set lblPunkty.Text to "Punkty: 0/5"
    • set btnOdpA.Visible to true (przywróć przyciski)
    • set btnOdpB.Visible to true
    • set btnOdpC.Visible to true
    • set btnNastepne.Visible to true
    • set btnStartOdNowa.Visible to false
    • call pokazPytanie
Zadanie 3, slajd 7/7
Zadanie 3: testowanie quizu na telefonie

Etap 10: testy na smartfonie

  1. Połącz telefon przez AI Companion.
  2. Przejdź przez wszystkie 5 pytań, testując każdą ścieżkę.
Test 1 (poprawna odpowiedź): Przy pytaniu 1 kliknij B) → lblFeedback: „✓ Dobrze!" (zielony), Punkty: 1/5.
Test 2 (błędna odpowiedź): Przy pytaniu 2 kliknij A) → lblFeedback: „✗ Źle! Poprawna: B" (czerwony), Punkty nadal 1/5.
Test 3 (blokada przycisków): Po udzieleniu odpowiedzi kliknij inny przycisk → nic się nie dzieje (przycisk nieaktywny).
Test 4 (kolejne pytanie): Kliknij „Następne" → pojawia się pytanie 2, lblNumer: „Pytanie 2/5".
Test 5 (koniec quizu): Po 5 pytaniach → lblPytanie: „Koniec quizu! Wynik: X/5", przyciski A/B/C znikają, pojawia się „Zagraj jeszcze raz".
Test 6 (restart): Kliknij „Zagraj jeszcze raz" → wraca do pytania 1, Punkty: 0/5.

Lista kontrolna

  • ✓ Pytania pojawiają się po kolei (1→5) z poprawnymi odpowiedziami z list
  • ✓ Poprawna odpowiedź → zielony komunikat + punkt
  • ✓ Błędna odpowiedź → czerwony komunikat + podpowiedź poprawnej
  • ✓ Po odpowiedzi przyciski są zablokowane (nie można klikać dalej)
  • ✓ Po 5 pytaniach pokazuje się wynik końcowy
  • ✓ Licznik punktów aktualizuje się na bieżąco
  • ✓ Restart przywraca pełny stan początkowy
  • ✓ Brak błędów/crashy
Zadanie 4, slajd 1/6
ZADANIE 4: Zmiana kolorów z losowaniem - cel
Cel zadania: wprowadzenie do zadania 4 - zmiana koloru tła z bloków, losowanie przez random integer i select list item.

Aplikacja Kolory v1

  • Cel aplikacji: trzy przyciski ustawiają stałe kolory tła, czwarty przycisk losuje kolor z listy sześciu dostępnych za każdym kliknięciem.
  • Poziom: łatwy - utrwalenie zmiany właściwości z bloków + wprowadzenie losowości.
  • Nowe pojęcia: blok random integer from ... to ..., lista kolorów (initialize global ... to list), blok select list item z losowym indeksem, zmiana Screen1.BackgroundColor z bloków.
  • Wymagania:
    • Trzy przyciski: Czerwony → tło Red, Zielony → Green, Niebieski → Blue.
    • Przycisk „Losuj kolor" → losuje jeden z 6 kolorów (Red, Green, Blue, Yellow, Orange, Purple).
    • Etykieta informuje o aktualnym kolorze.
    • Wynik losowania może się powtórzyć (to normalne - losowanie z powtórzeniem).
  • Komponenty: 4× Button, 1× Label (info).
  • Prezentacja bazowa: prezentacja 3, slajdy 19-24 (Przykład 3: zmiana kolorów).
Zadanie 4, slajd 2/6
Zadanie 4: nowy projekt i przyciski kolorów

Etap 1: utworzenie projektu

  1. Projects → Start new project, nazwa: kolory_v1.
  2. Screen1: Title = Kolory, AlignHorizontal = Center, BackgroundColor = White.

Etap 2: etykieta informacyjna

  1. Dodaj Label → Rename na lblInfo:
    • Text = Aktualny kolor: biały, FontSize = 18, Width = Fill parent, TextAlignment = Center

Etap 3: cztery przyciski w układzie poziomym

  1. Dodaj HorizontalArrangement1 (Layout) → Width = Fill parent.
  2. Wewnątrz umieść 3 przyciski:
    • btnCzerwony: Text = Czerwony, BackgroundColor = Light Red (lub #FFCDD2), FontSize = 16, Width = Fill parent, Height = 60
    • btnZielony: Text = Zielony, BackgroundColor = Light Green (lub #C8E6C9), Width = Fill parent, Height = 60
    • btnNiebieski: Text = Niebieski, BackgroundColor = Light Blue (lub #BBDEFB), Width = Fill parent, Height = 60
  3. Dodaj btnLosuj (poza układem, na całą szerokość):
    • Text = 🎲 Losuj kolor, BackgroundColor = Yellow, FontSize = 18, Width = Fill parent, Height = 55
Tło przycisków w Designerze to tylko podpowiedź wizualna - właściwy kolor tła ekranu ustawiamy z bloków. Kolory przycisków pomagają użytkownikowi zidentyfikować funkcję, ale nie mają związku z logiką programu.
Zadanie 4, slajd 3/6
Zadanie 4: bloki - stałe kolory

Etap 4: zdarzenia stałych kolorów w Blocks

  1. Przejdź do okna Blocks.
  2. btnCzerwony.Click:
    • set Screen1.BackgroundColor to Red (blok z listy Screen1, kolor z sekcji Colors)
    • set lblInfo.Text to "Aktualny kolor: czerwony"
  3. btnZielony.Click:
    • set Screen1.BackgroundColor to Green
    • set lblInfo.Text to "Aktualny kolor: zielony"
  4. btnNiebieski.Click:
    • set Screen1.BackgroundColor to Blue
    • set lblInfo.Text to "Aktualny kolor: niebieski"
Kod słownie (btnCzerwony): KIEDY kliknięto btnCzerwony, USTAW Screen1.BackgroundColor na Red ORAZ USTAW lblInfo.Text na „Aktualny kolor: czerwony".
Wskazówka: zbuduj najpierw jedno zdarzenie (btnCzerwony), sprawdź, czy działa, a potem skopiuj je prawym przyciskiem → Duplicate dwa razy i zmień tylko nazwę zdarzenia oraz wartość koloru/tekstu. To znacznie szybsze niż budowanie od nowa.
Zadanie 4, slajd 4/6
Zadanie 4: losowanie koloru - lista i random integer

Etap 5: lista kolorów i blok losujący

  1. Utwórz listę globalną: initialize global kolory to list.
  2. Dodaj 6 elementów do listy - przeciągnij z sekcji Colors wartości kolorów (Red, Green, Blue, Yellow, Orange, Purple) do slotów listy. Kolejność dowolna, ale zapisz ją - będzie odpowiadać numerom 1-6.
Lista kolory: pozycja 1=Red, 2=Green, 3=Blue, 4=Yellow, 5=Orange, 6=Purple. Zakres losowania musi być: from 1 to 6.

Etap 6: zdarzenie btnLosuj.Click

  1. Weź when btnLosuj.Click do.
  2. Wewnątrz wpnij: set Screen1.BackgroundColor to ...
  3. Jako wartość koloru wpnij blok select list item (z sekcji Lists):
    • list: wpnij get global kolory
    • index: wpnij blok random integer from 1 to 6 (z sekcji Math)
  4. Dodaj: set lblInfo.Text to join("Aktualny kolor (los:", random integer from 1 to 6, ")") - ALE UWAGA: to wylosowałoby INNĄ liczbę niż ta użyta do wyboru koloru! Dlatego lepiej:
  5. Optymalnie - najpierw zapisz los do zmiennej:
    • initialize global los to 0
    • W btnLosuj.Click: set global los to random integer from 1 to 6
    • Potem: set Screen1.BackgroundColor to select list item global kolory, get global los
    • I: set lblInfo.Text to join("Aktualny kolor (los #", get global los, ")")
WAŻNA PUŁAPKA: jeśli użyjesz random integer DWÓCH RAZY (raz do koloru, raz do tekstu), wylosujesz DWIE RÓŻNE liczby i tekst nie będzie zgadzał się z kolorem! Zawsze zapisuj wynik losowania do zmiennej i używaj jej wielokrotnie.
Zadanie 4, slajd 5/6
Zadanie 4: testowanie i wariant z ListPicker

Etap 7: testy na telefonie

  1. Połącz telefon przez AI Companion.
  2. Kliknij każdy przycisk i sprawdź efekt.
Test 1: Kliknij „Czerwony" → tło ekranu czerwone, lblInfo: „Aktualny kolor: czerwony".
Test 2: Kliknij „Zielony" → tło zielone.
Test 3: Kliknij „Niebieski" → tło niebieskie.
Test 4: Kliknij „Losuj kolor" 10 razy → za każdym razem inny (lub ten sam) kolor z puli 6, lblInfo pokazuje numer losu zgodny z kolorem.
Test 5: Sprawdź, czy lblInfo ZAWSZE zgadza się z faktycznym kolorem tła (jeśli nie - sprawdź, czy random integer jest zapisywany do zmiennej przed użyciem).

Rozszerzenie: wariant z ListPicker

  1. Zamiast trzech przycisków użyj jednego ListPicker z Elements = czerwony, zielony, niebieski, żółty, pomarańczowy, fioletowy.
  2. W zdarzeniu lstKolor.AfterPicking odczytaj get Selection.
  3. Zamień nazwę tekstu na wartość koloru przez if/else if/else (6 gałęzi).
  4. Efekt: jeden komponent zamiast trzech przycisków - kod krótszy i łatwiejszy do rozbudowy (dodanie koloru = dopisanie pozycji na liście + jedna gałąź).
Lista kontrolna: ✓ Trzy stałe kolory działają ✓ Losowanie daje kolor z puli 6 ✓ lblInfo zgadza się z kolorem ✓ Brak błędów przy 10 kliknięciach Losuj.
Zadanie 4, slajd 6/6
Zadanie 4: wnioski i podsumowanie

Czego uczy to zadanie

  • Właściwości można zmieniać dynamicznie z bloków - wygląd nie jest statyczny i może reagować na użytkownika.
  • Listy pozwalają zgrupować wiele wartości w jednej strukturze zamiast wielu zmiennych.
  • Blok random integer generuje liczbę z zakresu - podstawa losowości w grach i quizach.
  • select list item pobiera element o danym numerze - most między losowaniem a wartością.
  • Wynik losowania zapisuj do zmiennej przed wielokrotnym użyciem - inaczej każde użycie da inną wartość.
  • Zakres losowania musi odpowiadać długości listy - spójność danych to podstawa poprawności.

Rozszerzenia do samodzielnego wykonania

  1. Dodaj 7. kolor do listy i zwiększ zakres losowania do 7.
  2. Zamiast zmiany tła ekranu zmieniaj kolor TEKSTU etykiety lblInfo.
  3. Dodaj licznik: ile razy kliknięto Losuj (zwiększaj zmienną przy każdym kliknięciu).
  4. Dodaj dźwięk przy losowaniu (komponent Sound z efektem .wav w Media).
  5. Wariant z ListPicker opisany na poprzednim slajdzie.
Następne zadanie (5) wprowadza komponenty Media: syntezator mowy (TextToSpeech) i rozpoznawanie mowy (SpeechRecognizer). To pierwsze zadanie, w którym aplikacja „mówi" i „słyszy".
Zadanie 5, slajd 1/7
ZADANIE 5: Syntezator mowy i sterowanie głosem - cel
Cel zadania: wprowadzenie do zadania 5 - połączenie TextToSpeech (tekst → głos) i SpeechRecognizer (głos → tekst) w jednej aplikacji.

Aplikacja Mówiący telefon v2

  • Cel aplikacji: część 1 - aplikacja czyta wpisany tekst na głos (syntezator mowy). Część 2 - użytkownik wydaje komendę głosową („czerwony", „zielony", „lewo", „prawo"), a aplikacja reaguje: zmienia kolor tła lub wyświetla komunikat, potwierdzając komendę głosem.
  • Poziom: średni - pierwsze zadanie z komponentami Media (mowa).
  • Nowe pojęcia: TextToSpeech (metoda Speak, właściwości Language/Country), SpeechRecognizer (metoda GetText, zdarzenie AfterGettingText, parametr result), porównanie napisów przez blok =.
  • Wymagania:
    • Pole tekstowe + przycisk „Powiedz" → syntezator czyta wpisany tekst (po polsku, jeśli urządzenie obsługuje).
    • Przycisk „Słuchaj" → nasłuch mowy, rozpoznany tekst wyświetlany w etykiecie.
    • Komendy: „czerwony" → tło czerwone, „zielony" → tło zielone, „lewo" → komunikat, „prawo" → komunikat.
    • Po rozpoznaniu komendy aplikacja POTWIERDZA ją głosem (Speak).
    • Błędne słowo nie powoduje crasha - aplikacja po prostu nie reaguje.
  • Komponenty: TextBox, Button (Powiedz), TextToSpeech, Button (Słuchaj), SpeechRecognizer, Label (odpowiedź).
  • WAŻNE: testuj NA PRAWDZIWYM TELEFONIE - w emulatorze syntezator mowy działa słabo i tylko po angielsku.
  • Prezentacja bazowa: prezentacja 3, slajdy 25-29; prezentacja 5, slajdy 18-20.
Zadanie 5, slajd 2/7
Zadanie 5: nowy projekt, sekcja czytania

Etap 1: utworzenie projektu

  1. Projects → Start new project, nazwa: glos_v1.
  2. Screen1: Title = Mówiący telefon, AlignHorizontal = Center.

Etap 2: sekcja 1 - czytanie tekstu

  1. lblTytul1 (Label): Text = 🔊 Wpisz tekst do przeczytania:, FontSize = 16, Width = Fill parent.
  2. txtTekst (TextBox): Hint = np. Witaj w App Inventor, FontSize = 20, Width = Fill parent, MultiLine = true, Height = 80.
  3. btnPowiedz (Button): Text = 🔊 Powiedz, BackgroundColor = Blue, TextColor = White, FontSize = 20, Width = Fill parent, Height = 55.

Etap 3: komponent TextToSpeech (niewidoczny)

  1. Z palety Media przeciągnij TextToSpeech na ekran (pojawi się w Components, ale nie na Viewrze).
  2. Zmień nazwę na TextToSpeech1.
  3. Ustaw właściwości:
    • Country: POL (kraj - dla polskiego głosu)
    • Language: pol (kod języka)
    • Pitch: 1 (domyślne - wysokość głosu, zakres 0-2)
    • SpeechRate: 1 (domyślne - tempo, zakres 0-2)
Uwaga: nie wszystkie telefony mają zainstalowany polski głos syntezatora. Jeśli wypowiedź brzmi nienaturalnie lub po angielsku, wejdź w Ustawienia telefonu → Języki i wpisy → Synteza mowy i pobierz polski pakiet głosu.
Zadanie 5, slajd 3/7
Zadanie 5: bloki sekcji czytania

Etap 4: zdarzenie btnPowiedz.Click

  1. Przejdź do okna Blocks.
  2. Weź when btnPowiedz.Click do.
  3. Wewnątrz wpnij: call TextToSpeech1.Speak (z listy TextToSpeech1).
  4. Jako argument (pole text) wpnij: get txtTekst.Text (z listy txtTekst).
Kod słownie: KIEDY kliknięto btnPowiedz, WYWOŁAJ TextToSpeech1.Speak z argumentem: tekst z pola txtTekst.

Etap 5: walidacja - puste pole

  1. Przed blokiem Speak dodaj warunek:
    • if get txtTekst.Text = "" then
    • then: call TextToSpeech1.Speak("Wpisz jakiś tekst") (zamiast crasha - syntezator powie komunikat)
    • else: call TextToSpeech1.Speak(get txtTekst.Text)

Etap 6: dodatkowy przycisk „Stop"

  1. Dodaj btnStop: Text = ⏹ Stop, BackgroundColor = Red.
  2. Blok: when btnStop.Click do → call TextToSpeech1.Stop (przerywa bieżącą wypowiedź).
Metoda Stop przerywa wypowiedź w trakcie - przydatna, gdy użytkownik zmienił zdanie lub tekst jest długi. Bez niej trzeba by czekać, aż syntezator skończy czytać.
Zadanie 5, slajd 4/7
Zadanie 5: sekcja słuchania - SpeechRecognizer

Etap 7: komponenty sekcji słuchania w Designerze

  1. lblTytul2 (Label): Text = 🎙 Komendy głosowe:, FontSize = 16, Width = Fill parent.
  2. lblKomendy (Label): Text = Dostępne: czerwony, zielony, lewo, prawo, FontSize = 13, TextColor = Gray, Width = Fill parent.
  3. btnSluchaj (Button): Text = 🎙 Słuchaj komendę, BackgroundColor = Purple (lub #9C27B0), TextColor = White, FontSize = 20, Width = Fill parent, Height = 55.
  4. lblOdpowiedz (Label): Text = Rozpoznano: –, FontSize = 18, Width = Fill parent, Height = 60, BackgroundColor = White.

Etap 8: komponent SpeechRecognizer (niewidoczny)

  1. Z palety Media przeciągnij SpeechRecognizer.
  2. Zmień nazwę na SpeechRecognizer1.
  3. Brak dodatkowych właściwości do ustawienia - komponent korzysta z systemowego silnika rozpoznawania mowy.
Przy pierwszym uruchomieniu telefon zapyta o ZGODĘ na mikrofon - musisz ją wyrazić, inaczej rozpoznawanie nie zadziała. Niektóre telefony wymagają też połączenia z internetem do rozpoznawania mowy (silnik chmurowy).
Zadanie 5, slajd 5/7
Zadanie 5: bloki - nasłuch i reakcja na komendę

Etap 9: uruchomienie nasłuchu

  1. when btnSluchaj.Click do → call SpeechRecognizer1.GetText (rozpoczyna nagrywanie i rozpoznawanie).

Etap 10: obsługa rozpoznanego tekstu - zdarzenie AfterGettingText

  1. Z listy SpeechRecognizer1 weź blok when SpeechRecognizer1.AfterGettingText do z parametrem result.
  2. Wewnątrz:
    • set lblOdpowiedz.Text to join("Rozpoznano: ", get result)
  3. Dodaj instrukcję if z gałęziami dla czterech komend:
    • if get result = "czerwony" then
      • set Screen1.BackgroundColor to Red
      • call TextToSpeech1.Speak("Zmieniam na czerwony")
    • else if get result = "zielony" then
      • set Screen1.BackgroundColor to Green
      • call TextToSpeech1.Speak("Zmieniam na zielony")
    • else if get result = "lewo" then
      • set lblOdpowiedz.Text to join(get lblOdpowiedz.Text, " → Skręcasz w LEWO")
      • call TextToSpeech1.Speak("Skręcasz w lewo")
    • else if get result = "prawo" then
      • set lblOdpowiedz.Text to join(get lblOdpowiedz.Text, " → Skręcasz w PRAWO")
      • call TextToSpeech1.Speak("Skręcasz w prawo")
    • else (nieznana komenda):
      • call TextToSpeech1.Speak("Nie rozumiem. Powiedz: czerwony, zielony, lewo albo prawo")
Uwaga na wielkość liter: porównanie „czerwony" ≠ „Czerwony" ≠ „CZERWONY". Blok = jest czuły na wielkość liter. Jeśli rozpoznawanie zwraca tekst z wielką literą na początku, dodaj blok downcase (z sekcji Text) do get result przed porównaniem.
Zadanie 5, slajd 6/7
Zadanie 5: testowanie na prawdziwym telefonie

Etap 11: testy - TYLKO na prawdziwym smartfonie

Emulator NIE nadaje się do tego zadania. Syntezator mowy w emulatorze działa słabo i tylko po angielsku, a rozpoznawanie mowy wymaga mikrofonu, którego emulator nie ma. Podłącz prawdziwy telefon przez AI Companion.
  1. Połącz telefon: Connect → AI Companion → zeskanuj kod QR.
  2. Upewnij się, że w telefonie wyrażono zgodę na mikrofon (Ustawienia → Aplikacje → MIT AI2 Companion → Uprawnienia → Mikrofon = Zezwalaj).
Test 1 (czytanie): Wpisz „Witaj w App Inventor" → kliknij Powiedz → telefon czyta tekst na głos.
Test 2 (puste pole): Wyczyść pole → kliknij Powiedz → syntezator mówi „Wpisz jakiś tekst".
Test 3 (Stop): Wpisz długi tekst → Powiedz → w trakcie czytania kliknij Stop → wypowiedź przerywana.
Test 4 (komenda czerwony): Kliknij Słuchaj → powiedz wyraźnie „czerwony" → tło zmienia się na czerwone + syntezator mówi „Zmieniam na czerwony".
Test 5 (komenda zielony): Analogicznie → tło zielone.
Test 6 (lewo/prawo): Powiedz „lewo" → komunikat + głosowe potwierdzenie.
Test 7 (nieznana komenda): Powiedz „stół" → syntezator: „Nie rozumiem. Powiedz: czerwony, zielony, lewo albo prawo".
Test 8 (wielkość liter): Powiedz „Czerwony" (z wielkiej litery) → sprawdź, czy działa (jeśli nie - dodaj downcase).

Lista kontrolna

  • ✓ Aplikacja czyta wpisany tekst na głos (polski głos na prawdziwym telefonie)
  • ✓ Puste pole → komunikat głosowy (nie crash)
  • ✓ Przycisk Stop przerywa wypowiedź
  • ✓ Komenda „czerwony" → tło czerwone + potwierdzenie głosem
  • ✓ Komenda „zielony" → tło zielone + potwierdzenie głosem
  • ✓ Komendy „lewo"/„prawo" → komunikat + głos
  • ✓ Nieznana komenda → podpowiedź (nie crash)
  • ✓ Rozpoznany tekst widoczny w lblOdpowiedz
Zadanie 5, slajd 7/7
Zadanie 5: wnioski i podsumowanie

Czego uczy to zadanie

  • TextToSpeech.Speak(text) - zamienia tekst na głos; argumentem może być stały napis lub zmienna.
  • SpeechRecognizer.GetText() - rozpoczyna nasłuch; wynik dostępny w zdarzeniu AfterGettingText (parametr result).
  • Pętla mowa ↔ tekst - dwie strony komunikacji głosowej w jednej aplikacji.
  • Porównanie napisów - blok = jest czuły na wielkość liter; warto używać downcase.
  • Gałąź else - zawsze obsługuj nieznane wejście (komunikat zamiast milczenia).
  • Testowanie na prawdziwym sprzęcie - emulator nie nadaje się do zadań z mikrofonem i syntezatorem.

Rozszerzenia do samodzielnego wykonania

  1. Dodaj komendy: „żółty", „fioletowy", „góra", „dół" - rozszerz if/else if o kolejne gałęzie porównania.
  2. Użyj bloku downcase (z szuflady Text) na parametrze result przed porównaniem, aby rozpoznawanie nie zależało od wielkości liter.
  3. Zaprojektuj tryb rozmowy: aplikacja najpierw Speak z pytaniem, potem GetText, a wynik ocenia w AfterGettingText - prosty quiz głosowy.
  4. Połącz to zadanie z Zadaniem 9: zamiast przechylenia telefonu steruj piłką komendami głosowymi „lewo", „prawo", „góra", „dół".
  5. Dodaj listę historii komend (global variable + add items to list) i etykietę pokazującą ostatnie trzy rozpoznane frazy.
Zasada interfejsu głosowego: po każdej nieznanej komendzie aplikacja musi podpowiedzieć, jakie komendy są akceptowalne. Milczenie po błędzie to najgorsze możliwe zachowanie - użytkownik nie wie, czy system nadal działa.
Zadanie 6, slajd 1/6
ZADANIE 6: Kostka do gry z potrząśnięciem - cel
Cel zadania: zbudować aplikację rzucającą wirtualną kostką - potrząśnięcie telefonu losuje wynik rzutu (1-6) i wyświetla go z animacją.

Idea zadania

  • Wejście: fizyczne potrząśnięcie smartfonem (zdarzenie czujnika ruchu), alternatywnie przycisk „Rzuć".
  • Przetwarzanie: wylosowanie liczby z zakresu 1-6 blokiem random integer, zapis do zmiennej.
  • Wyjście: duża cyfra kostki na ekranie + komunikat głosowy (opcjonalnie) + wibracja.
  • Zdarzenie kluczowe: when AccelerometerSensor1.Shaking do - wykrywa wstrząs.
  • Efekt: telefon zachowuje się jak prawdziwa kostka do gry.
Uwaga sprzętowa: potrząśnięcie działa tylko na prawdziwym smartfonie - emulator Androida nie ma akcelerometru. Do testów użyj AI Companion z telefonem.
Zadanie 6, slajd 2/6
Zadanie 6: nowy projekt i komponent czujnika

Etap 1: utworzenie projektu

  1. Wejdź na ai2.appinventor.mit.edu i zaloguj się kontem Google.
  2. Menu Projects → Start new project.
  3. Nazwa projektu: Kostka (bez spacji i polskich znaków - App Inventor akceptuje tylko ASCII w nazwach plików).
  4. Po utworzeniu otworzy się pusty Screen1 w oknie Designer.

Etap 2: właściwości ekranu Screen1

  1. Zaznacz Screen1 w panelu Components.
  2. Ustaw Title = Kostka do gry.
  3. Ustaw BackgroundColor = Black (ciemne tło jak na stole do gry).
  4. Ustaw AlignHorizontal = 3: Center - cała zawartość wyśrodkowana poziomo.
  5. Ustaw AlignVertical = 3: Center - oraz pionowo.
  6. Wyłącz ShowStatusBar = false, aby aplikacja wyglądała jak pełnoekranowa gra.

Etap 3: komponent AccelerometerSensor

  1. Z palety Sensors przeciągnij AccelerometerSensor na ekran (niewidoczny na Viewrze, pojawi się w Components).
  2. Zmień nazwę na AccelerometerSensor1.
  3. Ustaw właściwości:
    • Available = true (informacja - czy czujnik jest dostępny, tylko do odczytu).
    • Enabled = true (czujnik aktywny - musi być zaznaczone).
    • Interval = 100 (ms - częstotliwość odczytów, 100 ms = 10 razy na sekundę).
    • Threshold = 3 (próg wstrząsu - im niższy, tym czujniejszy; domyślnie 3).
Wartość Threshold: akcelerometr mierzy przyspieszenie w m/s². Zdarzenie Shaking wywołuje się, gdy przekroczenie progu jest gwałtowne. Próg 3 to wartość balansująca: lekkie muśnięcie nie rzuca kostką, ale energiczne potrząśnięcie - już tak. Eksperymentuj w przedziale 2-5.
Zadanie 6, slajd 3/6
Zadanie 6: interfejs ekranu głównego

Etap 4: elementy widoczne w Designerze

  1. lblTytul (Label): Text = 🎲 KOSTKA, FontSize = 28, TextColor = White, Width = Fill parent, Height = 50.
  2. lblWynik (Label): Text = ?, FontSize = 96 (bardzo duża cyfra), TextColor = Yellow, Width = Fill parent, Height = 200, BackgroundColor = Black.
  3. lblPodpis (Label): Text = Potrząśnij telefonem!, FontSize = 16, TextColor = Gray, Width = Fill parent.
  4. btnRzuc (Button): Text = =Rzuć kostką, FontSize = 22, BackgroundColor = Red, TextColor = White, Width = 30%, Height = 60. (przycisk awaryjny - na emulatorze lub gdy czujnik zawodzi)

Etap 5: kolejność na ekranie

  1. Upewnij się, że kolejność w panelu Components to: lblTytul, lblWynik, lblPodpis, btnRzuc, AccelerometerSensor1.
  2. Etykiety powinny mieć Height ustawiony na wartość liczbową lub Fill parent - inaczej duża cyfra może się uciąć.
  3. Dla lblWynik ustaw też FontBold = true dla lepszej czytelności.
Rozmiar czcionki 96: jeśli etykieta nie wyświetla całej cyfry, sprawdź Height. Przy FontSize 96 potrzeba około 150-200 px wysokości. Alternatywnie zmniejsz do 72 - wciąż będzie dobrze widoczna.
Zadanie 6, slajd 4/6
Zadanie 6: zmienna i logika losowania

Etap 6: zmienna globalna

  1. Przejdź do okna Blocks.
  2. Z szuflady Variables weź initialize global name to.
  3. Zmień nazwę name na ostatniWynik.
  4. Jako wartość początkową wpnij pusty tekst "" (z szuflady Text) - lub 0 z Math, byle konsekwentnie.

Etap 7: procedura losowania

  1. Z szuflady Procedures weź procedure rzucKostke.
  2. Wewnątrz procedury umieść:
    • set global ostatniWynik to + blok random integer from 1 to 6 (z Math).
    • set lblWynik.Text to + get global ostatniWynik.
    • Opcjonalnie: set lblPodpis.Text to + join("Wypadło: " get global ostatniWynik).

Etap 8: wywołanie z przycisku

  1. Weź when btnRzuc.Click do.
  2. Wewnątrz wpnij blok call rzucKostke (z Procedur).
Kod słownie: PROCEDURA rzucKostke: losuj liczbę 1-6, zapisz w ostatniWynik, pokaż ją w lblWynik. KIEDY kliknięto btnRzuc, WYWOŁAJ rzucKostke.

Dlaczego procedura zamiast wstawienia bloków w Click?

  • Bloki losowania będą potrzebne DWA razy: raz dla przycisku, raz dla potrząśnięcia (Zadanie 6 wymaga obu ścieżek).
  • Procedura to funkcja y=f(x) z matematyki - piszesz raz, wywołujesz wiele razy.
  • Jeśli później zmienisz zakres (np. kostkę 20ścienną), zmieniasz w jednym miejscu.
Zadanie 6, slajd 5/6
Zadanie 6: reakcja na potrząśnięcie

Etap 9: zdarzenie Shaking - serce aplikacji

  1. Z listy AccelerometerSensor1 wedź blok when AccelerometerSensor1.Shaking do.
  2. Wewnątrz wpnij call rzucKostke - tyle! Cała logika jest już w procedurze.

Etap 10: wibracja jako informacja zwrotna

  1. Z listy AccelerometerSensor1 weź call AccelerometerSensor1.Vibrate (jeśli dostępny) lub dodaj komponent Vibrationsensor / użyj Sound.
  2. W App Inventor 2 zdarzenie potwierdzenia: po rzucie ustaw lblPodpis.Text na join("Wypadło: " ostatniWynik) - wizualne potwierdzenie odczytu.
  3. Opcjonalnie Sound: call Sound1.PlayFile z plikiem click.wav w Assets - krótki dźwięk kostki.

Etap 11: zabezpieczenie przed wielokrotnym rzutem

  1. Dodaj zmienną licznikRzutow zainicjalizowaną 0.
  2. W procedurze rzucKostke: set licznikRzutow to get licznikRzutow + 1.
  3. W lblPodpis pokazuj też liczbę rzutów: join("Rzut #" licznikRzutow " → " ostatniWynik).
Wiele zdarzeń z rzędu: energiczne potrząśnięcie może wywołać zdarzenie Shaking kilka razy pod rząd (czujnik raportuje każdy wstrząs powyżej progu). Efekt: kostka „mruga" zmieniając wynik. Rozwiązanie zaawansowane: po rzucie wyłącz czujnik (set Enabled to false), a po 1 sekundzie Timer włącz go ponownie - rzut trwa wtedy dokładnie raz.
Zadanie 6, slajd 6/6
Zadanie 6: testowanie i podsumowanie

Etap 12: testy na prawdziwym telefonie

Wymóg: potrząśnięcie testujemy wyłącznie na smartfonie z akcelerometrem. Na emulatorze działaj przyciskiem „Rzuć" - sprawdzi to samo losowanie i interfejs.
Test 1: Kliknij „Rzuć" → pojawia się losowa cyfra 1-6.
Test 2: Potrząśnij telefonem → cyfra zmienia się bez dotykania ekranu.
Test 3: Rzuć 10 razy → sprawdź, czy występują różne wyniki (prawdopodobieństwo braku szóstki w 10 rzutach ≈ 16%).
Test 4: Połóż telefon na stole, uderz w blat → sprawdź, czy Threshold nie wywołuje fałszywego rzutu.
Test 5: Jeśli kostka „mruga" - włącz blokadę Timer z Etapu 11.
Test 6: Sprawdź licznik rzutów w lblPodpis.

Lista kontrolna

  • ✓ Przycisk „Rzuć" losuje 1-6 i aktualizuje ekran
  • ✓ Potrząśnięcie telefonem losuje bez użycia przycisku
  • ✓ Duża cyfra widoczna i czytelna na czarnym tle
  • ✓ Czujnik włączony (Enabled = true) przy starcie aplikacji
  • ✓ Brak fałszywych rzutów od lekkich uderzeń (kalibracja Threshold)

Czego uczy to zadanie

  1. Zdarzenia czujników (Shaking) - wejście asynchroniczne niezależne od dotyku ekranu.
  2. Procedury - wielokrotne użycie tej samej logiki (przycisk + czujnik).
  3. Losowanie - blok random integer z szuflady Math.
  4. Kalibracja parametrów czujnika (Threshold, Interval) - dopasowanie do fizyki urządzenia.
  5. Separacja stanu (zmienna) od widoku (etykieta).
Rozszerzenia: (a) kostka 20ścienna - random integer from 1 to 20, (b) dwie kostki - dwie etykiety i dwa wylosowania, (c) historia 10 rzutów na liście, (d) ogłaszanie wyniku przez TextToSpeech, (e) animacja losowania - Timer przez 1 s pokazuje losowe cyfry, potem zatrzymuje się na wyniku.
Zadanie 7, slajd 1/6
ZADANIE 7: Poziomica cyfrowa z akcelerometru - cel
Cel zadania: zbudować cyfrową poziomicę, która na podstawie odczytów akcelerometru wyświetla przechylenie telefonu w stopniach i wskazuje, czy powierzchnia jest pozioma.

Idea zadania

  • Wejście: ciągłe odczyty przyspieszenia w osiach X i Y (zdarzenie AccelerationChanged).
  • Przetwarzanie: obliczenie kąta przechylenia - funkcja tan (tangens) z przyspieszenia.
  • Wyjście: etykieta z kątem w stopniach + wskaźnik „POZIOMO!" gdy |kąt| < 1°.
  • Zdarzenie kluczowe: when AccelerometerSensor1.AccelerationChanged z parametrami xAccel, yAccel, zAccel.
  • Efekt: telefon zachowuje się jak poziomica stolarska.
Różnica z Zadaniem 6: kostka reaguje na zdarzenie Shaking (impuls), poziomica czyta ciągłe wartości z AccelerationChanged (strumień danych). To dwa różne tryby pracy tego samego czujnika.
Zadanie 7, slajd 2/6
Zadanie 7: projekt i komponenty czujnika

Etap 1: nowy projekt

  1. Menu Projects → Start new project, nazwa: Poziomica.
  2. Screen1: Title = Poziomica cyfrowa, BackgroundColor = Dark Gray lub gradient - neutralne tło ułatwia ocenę „poziomu".
  3. AlignHorizontal = 3 Center, AlignVertical = 3 Center.

Etap 2: AccelerometerSensor - inne ustawienia niż w kostce

  1. Z palety Sensors przeciągnij AccelerometerSensor, nazwa AccelerometerSensor1.
  2. Ustaw:
    • Enabled = true.
    • Interval = 50 (ms - szybsze odświeżanie niż w kostce, bo kąt ma być płynny).
    • Threshold - tu NIEISTOTNY (dotyczy tylko zdarzenia Shaking, którego nie używamy).

Etap 3: dodatkowy czujnik dla dokładności (opcjonalnie)

  1. Możesz dodać MagneticFieldSensor do kompasu - ale do poziomicy wystarczy akcelerometr.
  2. Na start pomijamy - wrócimy, jeśli będziemy chcieli poziomicę z kompasem (pomiar azymutu).
Interval 50 vs 100: poziomica ma pokazywać PRĄŻCIWY ruch - dlatego częstsze odczyty (50 ms = 20 FPS). Kostka reaguje na impuls - 100 ms wystarczy. To samo urządzenie, dwa różne wymagania czasowe.
Zadanie 7, slajd 3/6
Zadanie 7: interfejs poziomicy

Etap 4: elementy widoczne

  1. lblTytul (Label): Text = 📐 POZIOMICA, FontSize = 24, TextColor = White, Width = Fill parent.
  2. lblKat (Label): Text = 0.0°, FontSize = 64, FontBold = true, TextColor = Lime, Width = Fill parent, Height = 120.
  3. lblStan (Label): Text = —, FontSize = 22, Width = Fill parent, Height = 60, BackgroundColor = Gray, TextColor = White.
  4. lblOś (Label): Text = Oś X: 0.0 | Oś Y: 0.0 | Oś Z: 0.0, FontSize = 13, TextColor = Silver, Width = Fill parent. (surowe odczyty - pomocne przy debugowaniu)
  5. lblInstrukcja (Label): Text = Połóż telefon na badanej powierzchni, FontSize = 14, TextColor = Gray, Width = Fill parent.

Etap 5: wskaźnik wizualny (opcjonalnie)

  1. Dodaj Canvas o Width = Fill parent, Height = 80, BackgroundColor = Black.
  2. Lub prościej: Slider (widżet do kontroli) ustawiony na min = -180, max = 180 - pokaże kąt pozycją suwaka.
  3. Na start zostaw same etykiety - Canvas rozbudujemy w rozszerzeniu.
Kontrast kolorów: jasnozielony (Lime) na ciemnym tle jest czytelny z daleka. Zmień tło na białe - i natychmiast stracisz czytelność w słońcu. Poziomica często używana jest na budowie / w ogrodzie, czyli w pełnym słońcu.
Zadanie 7, slajd 4/6
Zadanie 7: obliczanie kąta - matematyka w blokach

Etap 6: procedura obliczKat

  1. Z Procedures weź procedure obliczKat z PARAMETRAMI: x i y (kliknij „zarządza parametrami" i dodaj dwa).
  2. Wewnątrz procedury:
    • set global katRad to + blok atan (z Math) wpięty w get y / get x lub użyj atan2 jeśli dostępne w Twojej wersji AI2.
    • set global katStopnie to + (get global katRad) * 180 / 3.14159265 (bloki Math: * i /).
    • set lblKat.Text to + join((round (get global katStopnie * 10) / 10) "°") - zaokrąglenie do 1 miejsca po przecinku + znak stopnia.

Etap 7: zdarzenie AccelerationChanged

  1. Z listy AccelerometerSensor1 weź when AccelerometerSensor1.AccelerationChanged do z parametrami xAccel, yAccel, zAccel.
  2. Wewnątrz wpnij call obliczKat z argumentami: x = get xAccel, y = get yAccel.
  3. Dodatkowo aktualizuj surowe odczyty: set lblOś.Text to + join("X: " xAccel " | Y: " yAccel " | Z: " zAccel).

Etap 8: zmienne globalne

  1. initialize global katRad to 0 (radiany).
  2. initialize global katStopnie to 0 (stopnie - do porównań).
Przykład działania: telefon poziomo leżący: xAccel≈0, yAccel≈0 → atan(0/0) problem! Dlatego stosujemy atan2(y,x) lub zabezpieczamy dzielenie przez zero. Przechylony o 45°: y/x = tan(45°) = 1 → atan(1) = π/4 rad = 45°.
Dzielenie przez zero: gdy x=0 (czyste przechylenie w osi Y), y/x jest nieokreślone. Blok atan2(x,y) (jeśli dostępny) rozwiązuje to automatycznie. Alternatywnie: warunek if x = 0 then kat = 90 * sign(y).
Zadanie 7, slajd 5/6
Zadanie 7: logika stanu POZIOMO / PRZECHYLO

Etap 9: wykrywanie poziomu

  1. W procedurze obliczKat (po obliczeniu katStopnie) dodaj warunek:
    • if abs(get global katStopnie) < 1 then (blok abs z Math, blok < z Logic)
    • then: set lblStan.Text to "✅ POZIOMO", set lblStan.BackgroundColor to Green, set lblKat.TextColor to Lime.
    • else if abs(get global katStopnie) < 5 then
    • then: set lblStan.Text to "≈ Prawie poziomo", BackgroundColor = Yellow.
    • else
    • then: set lblStan.Text to join("PRZECHYŁ: " ... "°"), BackgroundColor = Red.

Etap 10: pomocnicza procedura aktualizujStan

  1. Wyciągnij logikę if do osobnej procedury aktualizujStan(kat) - jeśli warunek zacznie się komplikować.
  2. Utrzymanie jednej odpowiedzialności w procedurze (obliczanie vs prezentacja) ułatwia późniejszą rozbudowę.

Etap 11: brzęczyk zakończenia pomiaru (opcjonalnie)

  1. Dodaj komponent Buzzer z palety Media, nazwa Buzzer1.
  2. Przejście z else do then (gdy |kąt| spada poniżej 1°): call Buzzer1.Buzz 500 (500 ms) - krótki sygnał dźwiękowy „jest poziomo".
  3. Aby brzęczenie nie powtarzało się setki razy na sekundę, zmienna poprzedniStan pilnuje, by buzz wykonywał się tylko przy PRZEJŚCIU stanu.
Brzęczyk bez zabezpieczenia: zdarzenie AccelerationChanged leci 20 razy na sekundę. call Buzzer.Buzz wewnątrz warunku bez blokady = ciągły, nieprzyjemny pisk. Zawsze pilnuj stanu „poprzedni stan" lub timer lockout.
Zadanie 7, slajd 6/6
Zadanie 7: testowanie poziomicy i podsumowanie

Etap 12: testy - potrzebna płaska powierzchnia

Test 1: Połóż telefon na stole (szkło, blat) → lblStan: „✅ POZIOMO", kąt ≈ 0°.
Test 2: Podnieś jedną krawędź na książkę (~2 cm przy 15 cm długości) → kąt ≈ 7-8°, stan czerwony.
Test 3: Obróć telefon o 90° (ekran w prawo) → kąt blisko ±90°, sprawdź czy atan2 dobrze rozróżnia strony.
Test 4: Trzymaj pionowo (ekran do góry, telefon stoi) → kąt ≈ 90° lub -90° zależnie od orientacji osi.
Test 5: Sprawdź surowe odczyty w lblOś - wartości w zakresie ±9.81 m/s².
Test 6: Porównaj z prawdziwą poziomicą duchową (poziomicą bąbelkową) - różnic powinno być < 1°.

Lista kontrolna

  • ✓ Kąt aktualizuje się płynnie przy przechylaniu (Interval=50)
  • ✓ Stan POZIOMO pojawia się tylko faktycznym poziomie (<1°)
  • ✓ Kolory tła zmieniają się zgodnie z trzema stanami
  • ✓ Brak błędów przy x=0 (samoobrona przed dzieleniem przez zero)
  • ✓ (Opcjonalnie) Buzzer sygnalizuje osiągnięcie poziomu raz, nie ciągiem

Czego uczy to zadanie

  1. Ciągły strumień danych z czujnika (AccelerationChanged) vs impuls (Shaking).
  2. Matematyka w blokach: atan/atan2, przeliczenie radiany→stopnie, wartość bezwzględna.
  3. Wielostanowa logika warunkowa if/else if/else z progami odcięcia.
  4. Zabezpieczenia przed błędami numerycznymi (dzielenie przez zero).
  5. Interfejs przyrządu pomiarowego: duży odczyt, stany kolorystyczne, feedback dźwiękowy.
Rozszerzenia: (a) wskaźnik bąbelka na Canvas (kółko przesuwające się wraz z kątem), (b) zapis pomiarów do TinyDB (historia położeń), (c) tryb pionowy vs poziomy - osobne wzory dla obu orientacji, (d) połączenie z kompasem - poziomica z azymutem do wyznaczania kierunku, (e) kalibracja „zero" - przycisk ustawiający bieżący odczyt jako punkt odniesienia.
Zadanie 8, slajd 1/9
ZADANIE 8: Notatnik ze zdjęciami i danymi trwałymi - cel
Cel zadania: zbudować notatnik, który zapisuje wpisy tekstowe wraz ze zdjęciem i współrzędnymi GPS, przechowuje je trwale w TinyDB i wyświetla jako listę.

Idea zadania

  • Wejście: tekst notatki (TextBox), zdjęcie z aparatu (Camera), lokalizacja (LocationSensor).
  • Przetwarzanie: pakowanie trzech danych w jeden wpis (lista / struktura) i zapis do bazy lokalnej.
  • Wyjście: lista zapisanych notatek z podglądem miniatur i współrzędnych.
  • Komponenty kluczowe: Camera, LocationSensor, TinyDB, List / ListView.
  • Efekt: aplikacja pamięta dane między uruchomieniami - prawdziwa baza lokalna.
Dane trwałe: unlike zmiennych, które żyją dopóki działa aplikacja, TinyDB zapisuje dane na dysku telefonu. Zamknięcie i ponowne uruchomienie nie kasuje notatek - to podstawowa różnica między stanem tymczasowym a trwałym.
Zadanie 8, slajd 2/9
Zadanie 8: nowy projekt i ekran wpisu

Etap 1: utworzenie projektu

  1. Projects → Start new project, nazwa: Notatnik.
  2. Screen1: Title = 📒 Mój notatnik, AlignHorizontal = 3 (Center).
  3. Rozważ Scrollable = true dla Screen1 - lista notatek może być długa.

Etap 2: sekcja wpisywania notatki

  1. lblNowa (Label): Text = Nowa notatka:, FontSize = 16, Width = Fill parent.
  2. txtTresc (TextBox): Hint = Wpisz treść notatki..., Width = Fill parent, Height = 100, MultiLine = true, FontSize = 16.
  3. btnZdjecie (Button): Text = 📷 Zrób zdjęcie, BackgroundColor = Blue, TextColor = White, Width = 45%, Height = 55.
  4. btnZapisz (Button): Text = 💾 Zapisz notatkę, BackgroundColor = Green, TextColor = White, Width = 45%, Height = 55.
  5. imgMiniatura (Image): Width = 120, Height = 120, Visible = false (pokaże się po zrobieniu zdjęcia).
  6. lblGPS (Label): Text = GPS: —, FontSize = 13, TextColor = Gray, Width = Fill parent.
Zadanie 8, slajd 3/9
Zadanie 8: komponenty Camera, LocationSensor i TinyDB

Etap 3: kamera

  1. Z palety Media przeciągnij Camera, nazwa Camera1.
  2. Brak właściwości do ustawienia - Camera to „przycisk wywołujący" aplikację aparatu systemowego.
  3. Uwaga: na emulatorze aparat zwykle NIE DZIAŁA - testuj na prawdziwym telefonie.

Etap 4: czujnik lokalizacji

  1. Z palety Sensors przeciągnij LocationSensor, nazwa LocationSensor1.
  2. Ustaw:
    • Enabled = true.
    • DistanceInterval = 0 (0 = raportuj zmianę co Interval, nie co dystans).
    • Interval = 1000 (ms - sekunda, wystarczająco dla notatnika).
    • Provider = network lub gps - network szybszy i mniej prądożerny, GPS dokładniejszy na otwartej przestrzeni.
  3. Pierwsze uruchomienie na telefonie: system zapyta o ZGODĘ na lokalizację - musisz zezwolić.

Etap 5: baza danych TinyDB

  1. Z palety Storage przeciągnij TinyDB, nazwa TinyDB1.
  2. Brak właściwości - to niewidoczny komponent przechowujący pary klucz-wartość.
  3. Konwencja nazewnicza: notatki będzie kluczem pod listę wszystkich wpisów.
Zgody systemowe: Camera i LocationSensor wymagają jawnej zgody użytkownika przy pierwszym użyciu. Aplikacja bez zgody dostanie wyjątek / pusty wynik. Poinformuj użytkownika w interfejsie, PO CO potrzebujesz tych uprawnień - to dobra praktyka i wymóg sklepów z aplikacjami.
Zadanie 8, slajd 4/9
Zadanie 8: obsługa aparatu - zdarzenia Camera

Etap 6: wywołanie aparatu

  1. W Blocks: when btnZdjecie.Click do.
  2. Wewnątrz: call Camera1.TakePicture - uruchamia aplikację aparatu.

Etap 7: odbiór zdjęcia

  1. Z listy Camera1 weź when Camera1.AfterPicture do z parametrem image (ścieżka do zrobionego pliku).
  2. Wewnątrz:
    • set imgMiniatura.Picture to get image - pokaż miniaturę.
    • set imgMiniatura.Visible to true.
    • set global sciezkaZdjecia to get image - zapamiętaj ścieżkę do zapisu w notatce.
  3. Inicjalizacja: initialize global sciezkaZdjecia to "".

Etap 8: odczyt lokalizacji

  1. Z listy LocationSensor1 zdarzenie when LocationSensor1.LocationChanged do z parametrem latitude, longitude.
  2. Wewnątrz:
    • set global szer do get latitude, set global dlug do get longitude (zmienne globalne).
    • set lblGPS.Text to + join("GPS: " latitude "° N, " longitude "° E") - uproszczona etykieta (dla południa/zachodu warto dodać S/W).
  3. Zmienne: initialize global szer to 0, initialize global dlug to 0.
Kolejność zdarzeń: użytkownik NAJPIERW klika „Zrób zdjęcie" (aplikacja wychodzi do aparatu), POTEM wraca i dopiero LocationSensor (pracujący w tle) dostarcza współrzędne. Nie zakładaj, że GPS już jest - sprawdzaj w momencie ZAPISU, czy szer/dlug mają sensowne wartości (np. ≠ 0).
Zadanie 8, slajd 5/9
Zadanie 8: zapis notatki do TinyDB - format danych

Etap 9: struktura jednego wpisu

Wpis notatki to lista czterech elementów w stałej kolejności: [tresc, sciezkaZdjecia, szerokosc, dlugosc]. Cała kolekcja notatek to lista list przechowywana w TinyDB pod kluczem notatki.
  1. Zmienne globalne:
    • initialize global listaNotatek to + create empty list (z Lists).
    • Po wczytaniu z bazy: set global listaNotatek to + call TinyDB1.GetValue tag "notatki" blockIfTagNotThere = create empty list.

Etap 10: budowa wpisu i zapis

  1. when btnZapisz.Click do:
  2. Warunek walidacji: if length of (get txtTresc.Text) = 0 then → call Notifier1.LogDetailedMessage lub ustaw lblGPS na ostrzeżenie i zakończ (return). Notatka bez treści nie ma sensu.
  3. set global nowyWpis to + add items to list:
    • lista = create empty list
    • item1 = get txtTresc.Text
    • item2 = get global sciezkaZdjecia
    • item3 = get global szer
    • item4 = get global dlug
  4. call add items to list (lista global listaNotatek, item get global nowyWpis) - doklejamy całą listę-wpis jako JEDEN element listy głównej.
  5. call TinyDB1.StoreValue tag "notatki" valueToStore get global listaNotatek - zapis trwały.
  6. Posprzątaj: set txtTresc.Text to "", set imgMiniatura.Visible to false, set global sciezkaZdjecia to "".
Zagnieżdżone listy: listaNotatek to lista, której KAŻDY element jest sam listą (wpisem). Indeksowanie: select list item listaNotatek index i → zwraca i-ty wpis; select item tego wpisu index 1 → treść. Ta struktura (lista list) jest kluczem do całego zadania.
Zadanie 8, slajd 6/9
Zadanie 8: lista notatek na ekranie

Etap 11: elementy listy

  1. lblLista (Label): Text = Zapisane notatki (0):, FontSize = 16, Width = Fill parent.
  2. lstNotatki (ListView): Width = Fill parent, Height = 200, BackgroundColor = White.
  3. Listy Androida (ListView) wyświetlają jedną kolumnę napisów - miniatury zdjęć pominmy w głównej liście (ograniczenie komponentu).
  4. Ewentualnie Notifier1 z palety User Interface do komunikatów (zamiast etykietek-pomocników).

Etap 12: procedura odswiezListe

  1. procedure odswiezListe:
    • Utwórz pomocniczą listę tekstową etykiety = empty list.
    • for index i from 1 to length of listaNotatek (pętla z Control):
      • set wpis to select list item (get global listaNotatek, get i).
      • set tresc to select list item (get wpis, 1).
      • set gpsCzesc to select list item (get wpis, 3) - pierwsza współrzędna dla skrótu.
      • Skróć treść: segment (get tresc) start 1 length 30 + dołącz „..." jeśli dłuższa (blok segment z Text).
      • add items to list (etykiety, item join("#" i " " skrot " [" gpsCzesc "]")).
    • set lstNotatki.Elements to get etykiety.
    • set lblLista.Text to join("Zapisane notatki (" length(global listaNotatek) "):").

Etap 13: wywołania procedury

  1. Wołaj odswiezListe: (a) po zapisie notatki, (b) przy zdarzeniu Screen1.Initialize.
  2. Screen1.Initialize: set global listaNotatek to call TinyDB1.GetValue "notatki" (empty list), potem odswiezListe.
Screen1.Initialize to zdarzenie uruchamiane raz - przy starcie aplikacji. Właśnie tu wczytujemy dane z TinyDB do pamięci roboczej. Bez tego lista byłaby pusta do czasu pierwszego zapisu, nawet jeśli baza ma dane z poprzedniej sesji.
Zadanie 8, slajd 7/9
Zadanie 8: odczyt pojedynczej notatki (ElementPicked)

Etap 14: zdarzenie wyboru z listy

  1. when lstNotatki.ElementPicked do z parametrem position (indeks wybranego elementu, od 1).
  2. Wewnątrz:
    • set global wybranyWpis to select list item (get global listaNotatek, get position).
    • set global wybranaTresc to select list item (get global wybranyWpis, 1).
    • set global wybranaSciezka to select list item (get global wybranyWpis, 2).
    • set global wybranaSzer to ... index 3, wybranyDlug ... index 4.

Etap 15: podgląd w Notifier (dialog)

  1. Z palety User Interface weź Notifier1.
  2. call Notifier1.ShowMessageDialog:
    • message = get global wybranaTresc
    • title = "Podgląd notatki"
    • buttonText = "OK"
  3. Lub pełniejszy podgląd: osobny ekran Screen2 z ImageView (jeśli sciezka ≠ ""), dużą treścią i współrzędnymi - na rozszerzenie.

Etap 16: pokazanie zdjęcia w podglądzie

  1. Przy ElementPicked ustaw set imgMiniatura.Picture to get global wybranaSciezka oraz Visible = true - miniatura nad listą zmienia się na zdjęcie wybranej notatki.
  2. Jeśli sciezka pusta (notatka bez zdjęcia): ustaw imgMiniatura.Visible = false lub placeholderowy tekst w lblGPS.
Indeksy pozycji: position z ElementPicked jest zgodne z kolejnością w Elements (1-pierwszy wiersz). Gdy listaNotatek i Elements są budowane w tej samej kolejności - pozycja z listy widocznej bezpośrednio indeksuje listę danych. To musi być utrzymane - inaczej podgląd pokaże inną notatkę niż kliknięta.
Zadanie 8, slajd 8/9
Zadanie 8: usuwanie i czyszczenie bazy

Etap 17: przycisk usuwania

  1. btnUsun (Button): Text = 🗑 Usuń zaznaczoną, BackgroundColor = Red, TextColor = White, Width = Fill parent, Height = 50.
  2. Logika w blokach:
    • Potrzebna zmienna global pozycjaWyboru - zapisywana w ElementPicked (set pozycjaWyboru to get position).
    • if pozycjaWyboru > 0 then:
      • call remove list index (global listaNotatek, index get global pozycjaWyboru).
      • call TinyDB1.StoreValue "notatki" ... - nadpisz bazę zaktualizowaną listą.
      • set pozycjaWyboru to 0, odswiezListe.
    • else: komunikat „Najpierw wybierz notatkę z listy".

Etap 18: czyszczenie całej bazy (ostrożnie!)

  1. btnWyczysc: Text = ⚠ Wyczyść wszystko.
  2. Potwierdzenie przez Notifier: call Notifier1.ShowChooseDialog z tekstem „Skasować WSZYSTKIE notatki?" - przyciski Tak / Nie.
  3. Obsługa zdarzenia afterChoosing(choice): if choice = "Tak" then:
    • call TinyDB1.ClearAll (lub ClearTag "notatki").
    • set global listaNotatek to create empty list.
    • odswiezListe.
Operacje nieodwracalne wymagają potwierdzenia. Jedno kliknięcie nie może kasować całej bazy. ShowChooseDialog to minimum akceptowalne w UI - przypomina wzorzec „czy na pewno?" znany z każdego systemu plików.
Zadanie 8, slajd 9/9
Zadanie 8: testowanie notatnika i podsumowanie

Etap 19: testy - TYLKO na prawdziwym telefonie

Wymagania sprzętowe: aparat i GPS nie działają na standardowym emulatorze. Podłącz telefon przez AI Companion. Udziel zgód na: lokalizację, aparat (i ewentualnie pamięć - depending od wersji Androida).
Test 1 (start): Uruchom pustą bazę → lista pusta, licznik 0, brak crasha.
Test 2 (zapis bez zdjęcia): Wpisz tekst → Zapisz → notatka na liście, sciezkaZdjecia pusta.
Test 3 (pełny wpis): Tekst → Zrób zdjęcie → potwierdź GPS (liczby ≠ 0) → Zapisz → 4 pola kompletne.
Test 4 (trwałość): Zamknij aplikację całkowicie → uruchom ponownie → lista jak przed zamknięciem (TinyDB przeżył!).
Test 5 (podgląd): Kliknij pozycję na liście → dialog z treścią + miniatura się zmienia.
Test 6 (usuwanie): Usuń notatkę → znika z listy → restart → nadal nie ma (zapisano do TinyDB).
Test 7 (walidacja): Puste txtTresc + Zapisz → ostrzeżenie, baza nietknięta.
Test 8 (wyczyść): Wyczyść wszystko → dialog TAK → lista pusta po resecie.

Lista kontrolna

  • ✓ Tekst + zdjęcie + GPS zapisują się jako jeden wpis
  • ✓ Lista poprawnie się odświeża (długość zgodna z bazą)
  • ✓ Dane PRZETRWAJĄ zamknięcie aplikacji (TinyDB)
  • ✓ Podgląd wskazuje tę samą notatkę, którą kliknięto
  • ✓ Usuwanie i czyszczenie działają z potwierdzeniem
  • ✓ Walidacja pustego tekstu blokuje zapis

Czego uczy to zadanie

  1. TinyDB - trwałe przechowywanie pary klucz-wartość na urządzeniu.
  2. Listy zagnieżdżone - modelowanie rekordów jako list o stałej kolejności pól.
  3. Pętle for + indeksowanie od 1 - przeglądanie kolekcji.
  4. Zdarzenia aplikacji zewnętrznych (Camera.AfterPicture) i czujników (LocationChanged).
  5. Cykl CRUD: Create (zapisz), Read (odśwież/podgląd), Update (nadpisz bazę), Delete (usuń).
Rozszerzenia: (a) pole DATY w każdym wpisie (clock.Now), (b) wyszukiwarka - filtruj listę po fragmencie tekstu, (c) eksport listy jako CSV (Web.StringToFile / Share), (d) mapa - WebViewer z OpenStreetMap i pinezką na współrzędnych, (e) Podcast głosowy - zamiast tekstu nagrywaj dźwięk (SoundRecorder) jako czwarty element wpisu.
Zadanie 9, slajd 1/9
ZADANIE 9: Gra „Łap piłkę" - sterowanie przechyleniem - cel
Cel zadania: zbudować grę zręcznościową: piłka spada z góry, gracz steruje koszykiem przechyleniem telefonu, łapanie zdobywa punkty, przepuszczenie kończy grę.

Idea zadania

  • Wejście: przechylenie telefonu (AccelerationChanged) - lewo/prawo jak kierownica.
  • Przetwarzanie: zegar Clock przesuwa piłkę w dół, kolizja z koszykiem = punkt.
  • Wyjście: Canvas z piłką i koszykiem, licznik punktów, ekran końca gry.
  • Komponenty kluczowe: Canvas, Ball, Sprite (koszyk), Clock, AccelerometerSensor.
  • Efekt: pełna gra zręcznościowa - pętla gry, kolizje, punktacja, restart.
Pętla gry: każda gra ma tę samą strukturę: (1) inicjalizacja, (2) pętla - aktualizacja stanu co klatkę, (3) detekcja zdarzeń (kolizje, wejście), (4) rysowanie, (5) warunek końca. App Inventor realizuje to zegarem Clock - jego tykanie JEST pętlą gry.
Zadanie 9, slajd 2/9
Zadanie 9: nowy projekt i Canvas

Etap 1: utworzenie projektu

  1. Projects → Start new project, nazwa: LapPilke.
  2. Screen1: Title = Łap piłkę!, BackgroundColor = Black, AlignHorizontal = 3, AlignVertical = 3.
  3. Wyłącz ShowStatusBar dla immersji pełnoekranowej.

Etap 2: pole gry Canvas

  1. Z palety Animation przeciągnij Canvas.
  2. Nazwa: Canvas1.
  3. Ustaw:
    • Width = Fill parent (szerokość ekranu).
    • Height = 350 px (pole gry - nie całą wysokość, bo nad nim będzie HUD).
    • BackgroundColor = Black.
    • PaintColor = White (domyślny kolor rysowania).

Etap 3: HUD nad polem gry

  1. lblWynik (Label): Text = Punkty: 0, FontSize = 22, FontBold = true, TextColor = Yellow, Width = Fill parent.
  2. lblKomunikat (Label): Text = Przechyl telefon, by łapać, FontSize = 14, TextColor = Gray, Width = Fill parent.
  3. Kolejność: lblWynik, lblKomunikat, Canvas1 - wynik nad polem gry.
Współrzędne Canvas: lewy górny róg to (0,0), oś X rośnie w prawo, oś Y w DÓŁ (jak w grafice komputerowej, nie jak w matematyce). Piłka „spadająca" ma rosnącą współrzędną Y. Poznaj tę konwencję zanim zaczniesz ustawić pozycje - zaoszczędzi wielu „dlaczego piłka idzie w górę?".
Zadanie 9, slajd 3/9
Zadanie 9: piłka (Ball) i koszyk (Sprite)

Etap 4: piłka - komponent Ball

  1. Z palety Animation przeciągnij Ball NA Canvas1 (musi być dzieckiem Canvas).
  2. Nazwa: Ball1.
  3. Właściwości:
    • Width = 30, Height = 30 (średnica piłki).
    • BackgroundColor = Yellow.
    • Enabled = false na start (nie reaguje na kolizje dopóki gra nie ruszy).
    • Speed = 0 (startujemy zatrzymaną, nadamy prędkość przy starcie gry).
    • Heading = 90 (w dół - w konwencji Ball heading 0 = prawo, 90 = dół).
    • X / Y ustawimy w blokach losowo przy każdym respawnie.

Etap 5: koszyk - komponent Sprite

  1. Z palety Animation przeciągnij Sprite NA Canvas1, nazwa Koszyk1.
  2. Właściwości:
    • Width = 90, Height = 40 (szeroki na tyle, by łapać).
    • BackgroundColor = Cyan lub obrazek (Picture - wg uznania).
    • Y = 300 (blisko dolnej krawędzi Canvas o Height 350; Y koszyka = 350 - 40 - 10).
    • X = środek: (szerokośćCanvas - 90) / 2 - ustawimy w Initialize.
    • Enabled = true, Visible = true.
    • Speed = 0 (koszyk nie spada - sterujemy go ręcznie ustawiając X).
Właściwość Heading w Ball: kąt w stopniach, 0 = w prawo, 90 = w dół (oś Y rośnie w dół!), 270 = w górę. Dla spadającej piłki heading = 90. Jeśli piłka „leci w górę" - sprawdź heading, nie Y.
Zadanie 9, slajd 4/9
Zadanie 9: zegar i pętla gry

Etap 6: komponent Clock

  1. Z palety Sensors przeciągnij Clock, nazwa Clock1.
  2. Właściwości:
    • TimerEnabled = false (gra nie tyka przed startem).
    • TimerInterval = 50 ms (20 klatek na sekundę - płynna gra, nie obciąża CPU).
  3. Opcjonalnie drugi Clock ClockSpawn z innym interwałem do generowania nowych piłek - na rozszerzenie.

Etap 7: zdarzenie Timer - serce pętli

  1. when Clock1.Timer do:
  2. W tej wersji piłka spada sama (Ball ma heading i prędkość) - Timer służy do:
    • Sprawdzenia, czy piłka wypadła poza dolną krawędź: if Ball1.Y > Canvas1.Height then → koniec gry / utrata życia.
    • Aktualizacji HUD (np. czas gry): set lblWynik.Text to join("Punkty: " global punkty).
    • Ewentualnie przesunięcia koszyka grawitacyjnie w dół (auto-center) - pomijamy.

Etap 8: stałe gry

  1. initialize global punkty to 0.
  2. initialize global zycia to 3 (tryb z 3 życiami zamiast natychmiastowego game over - opcjonalnie).
  3. initialize global graTrwa to false (blokada wejścia przed startem).
TimerInterval 50 vs 16: 50 ms (20 FPS) wystarczy dla spadającej piłki i jest oszczędne dla baterii. 16 ms (60 FPS) przyda się przy szybkich grach akcji - kosztem prądu. Dla tego zadania 50 ms to dobry kompromis - zmierz samodzielnie porównując płynność 30/50/100.
Zadanie 9, slajd 5/9
Zadanie 9: sterowanie przechyleniem - akcelerometr

Etap 9: czujnik do sterowania

  1. Z palety Sensors przeciągnij AccelerometerSensor, nazwa AccelerometerSensor1.
  2. Właściwości: Enabled = true, Interval = 50 (jak Clock - synchronizacja częstotliwości), Threshold - nieistotny (nie używamy Shaking).

Etap 10: przeliczanie przechylenia na ruch koszyka

  1. when AccelerometerSensor1.AccelerationChanged do (xAccel, yAccel, zAccel):
  2. if graTrwa = true then (nie steruj przed startem / po końcu):
  3. Przesunięcie proporcjonalne do przechylenia w osi X:
    • set przesuniecie to (get xAccel) * 8 - mnożnik 8 skaluje m/s² na piksele/tick. Eksperymentuj 5-15.
    • set Koszyk1.X to (get Koszyk1.X) + get przesuniecie.
    • Granice: if Koszyk1.X < 0 then set X to 0; if Koszyk1.X + Koszyk1.Width > Canvas1.Width then set X to (Canvas1.Width - Koszyk1.Width).

Etap 11: alternatywa - przyciski dotykowe

  1. Dla testów na emulatorze / dla dostępności dodaj btnLewo i btnPrawo pod Canvas.
  2. when btnLewo.Click do: set Koszyk1.X to (X - 40) (z blokadą granic jak wyżej).
  3. Ta ścieżka pozwala grać BEZ czujnika - niezależność wejścia to dobra praktyka (jak przycisk „Rzuć" w kostce).
Kalibracja mnożnika: xAccel w spoczynku ≈ 0. Przechylenie o ~30° daje ~5 m/s². Mnożnik 8 → 40 px na tick - czyli koszyk przeskakuje 40 pikseli co 50 ms = 800 px/s, czyli praktycznie pełna szerokość ekranu w mgnieniu oka. Zbyt duży mnożnik = chaotyczne migotanie; zacznij od 5 i zwiększaj.
Zadanie 9, slajd 6/9
Zadanie 9: punktacja - kolizja Ball z koszykiem

Etap 12: zdarzenie kolizji

  1. Z listy Koszyk1 weź when Koszyk1.CollidedWith do z parametrem other (obiekt, w który uderzył koszyk).
  2. Wewnątrz:
    • if get other = get Ball1 then (porównanie referencji - łapiemy TYLKO piłkę, nie inne sprite'y).
    • then: set global punkty to (get global punkty) + 1.
    • set lblWynik.Text to join("Punkty: " get global punkty).
    • call respawn (procedura - nowa piłka u góry).
    • Opcjonalnie: przyspiesz - set Ball1.Speed to (Ball1.Speed + 1) - trudność rośnie z wynikiem.

Etap 13: procedura respawn (nowa piłka)

  1. procedure respawn:
    • set Ball1.Enabled to false (krótki „oddech" - zapobiega wielokrotnym kolizjom w jednej klatce).
    • set Ball1.X to random integer from 20 to (Canvas1.Width - Ball1.Width - 20).
    • set Ball1.Y to 0 (szczyt).
    • set Ball1.Heading to 90 (w dół), set Ball1.Speed to global bazowaSzybkosc (zmienna, np. 8).
    • set Ball1.Enabled to true.
Problem podwójnej kolizji: bez Enabled=false→true piłka może zarejestrować 2-3 kolizje w sąsiadujących klatkach (50 ms), bo nadal dotyka koszyka po odbiciu. Efekt: +3 punkty za jedno złapanie. Blokada Enabled podczas respawnu to najprostszy debounce kolizji.
Zadanie 9, slajd 7/9
Zadanie 9: warunek końca gry i restart

Etap 14: wykrycie przegranej

  1. W when Clock1.Timer (po sprawdzeniu granicy):
    • if Ball1.Y > Canvas1.Height then (piłka spadła obok koszyka):
    • then: call koniecGry.

Etap 15: procedura koniecGry

  1. procedure koniecGry:
    • set global graTrwa to false.
    • set Clock1.TimerEnabled to false (zatrzymaj pętlę).
    • set Ball1.Enabled to false, set AccelerometerSensor1.Enabled to false (zablokuj sterowanie).
    • set lblKomunikat.Text to join("KONIEC GRY! Wynik: " global punkty), TextColor = Red.
    • Rekord: if punkty > global rekord then → set rekord to punkty + komunikat „NOWY REKORD!".
    • call TinyDB1.StoreValue tag "rekord" valueToStore get global rekord - rekord trwały! (mechanizm z Zadania 8).
    • Pokaż btnStart (Visible = true), zmień Text na „Zagraj jeszcze raz".

Etap 16: restart - procedura startGry

  1. procedure startGry:
    • set global punkty to 0, set graTrwa to true.
    • set lblWynik.Text to "Punkty: 0", lblKomunikat na domyślny szary.
    • set Koszyk1.X to ((Canvas1.Width - Koszyk1.Width) / 2) (środek).
    • set Ball1.Speed to global bazowaSzybkosc, call respawn.
    • set Clock1.TimerEnabled to true, set AccelerometerSensor1.Enabled to true.
    • set btnStart.Visible to false (ukryj przycisk startu podczas gry).
  2. when btnStart.Click do → call startGry.
  3. when Screen1.Initialize do: wczytaj rekord z TinyDB (set rekord to GetValue "rekord" 0), ustaw btnStart.Text = "▶ START", Visible = true, graTrwa = false.
Trzy stany gry: (1) menu/start - przycisk widoczny, czujnik wyłączony; (2) gra - Timer tyka, czujnik aktywny; (3) koniec - Timer stop, komunikat, rekord zapisany. Każde wejście (klik, czujnik) MUSI sprawdzać graTrwa - inaczej gracz będzie sterował koszykiem w menu.
Zadanie 9, slajd 8/9
Zadanie 9: testowanie gry

Etap 17: testy - telefon + emulator

Część gry działa na emulatorze (przyciski dotykowe, kolizje, punktacja), ale sterowanie przechyleniem - tylko na telefonie. Testuj obie ścieżki: najpierw logikę na emulatorze, potem tilt na prawdziwym urządzeniu przez AI Companion.
Test 1 (start): Uruchom → widzisz menu, przycisk START, piłka nie spada.
Test 2 (ruch): START → piłka spada z góry, koszyk stoi na środku.
Test 3 (sterowanie - tilt): Przechyl telefon w lewo → koszyk jedzie w lewo; w prawo → w prawo. Sprawdź granice (nie wyjeżdża poza Canvas).
Test 4 (sterowanie - przyciski): Jeśli dodane btnLewo/Prawo → działają na emulatorze.
Test 5 (punktacja): Złap piłkę → „Punkty: 1", piłka respawnowana u góry.
Test 6 (podwójny punkt): Złap i sprawdź czy NIE dostajesz +2/+3 za jedno złapanie (debounce).
Test 7 (przegrana): Nie łap → piłka mija → KONIEC GRY, Timer stop, komunikat czerwony.
Test 8 (rekord): Zagraj ponownie, pobij wynik → „NOWY REKORD!". Zamknij aplikację → otwórz → rekord pamięta (TinyDB).
Test 9 (restart): Po końcu kliknij START → punkty 0, piłka leci, czujnik działa.

Lista kontrolna

  • ✓ Trzy stany gry rozłączają poprawnie wejścia i Timer
  • ✓ Tilt steruje płynnie z kalibrowanym mnożnikiem
  • ✓ Kolizja = dokładnie +1 punkt (bez multiplikacji)
  • ✓ Spadek obok = koniec + zatrzymanie pętli
  • ✓ Rekord przetrwa zamknięcie aplikacji
  • ✓ Restart resetuje WSZYSTKO (punkty, pozycje, prędkość)
Zadanie 9, slajd 9/9
Zadanie 9: podsumowanie gry

Czego uczy to zadanie

  1. Pętla gry na Clock - Timer jako fixed timestep update; TimerInterval = klatka.
  2. Obiekty z fizyką - Ball/Sprite mają Speed, Heading, kolizje - nie wszystko trzeba liczyć ręcznie.
  3. Wejście analogowe - przechylenie → wektor ruchu; kalibracja mnożnika to serce tilt control.
  4. Kolizje i debounce - CollidedWith + blokada Enabled zapobiega multi-punktacji.
  5. Maszyna stanów - menu / gra / koniec; każde wejście warunkowane przez graTrwa.
  6. Trwały rekord - TinyDB (z Zadania 8) przeniesiony do kontekstu gry.
  7. Progresja trudności - rosnąca prędkość za punkt, krzywa flow.

Architektura gry w jednym schemacie

startGry (reset stanu, włączenie Timer+czujnika) → Clock.Timer (aktualizacja świata, test granic) → AccelerationChanged (sterowanie, jeśli graTrwa) → CollidedWith (punktacja + respawn) → koniecGry (stop, rekord w TinyDB, powrót do menu).
Rozszerzenia: (a) trzy życia zamiast natychmiastowego końca, (b) bonus zielony (+5 pkt, szybszy), (c) przeszkody spadające równolegle do piłek, (d) poziomy trudności - osobna zmienna bazowaSzybkosc w menu, (e) dźwięki łapania/przegranej (Sound + pliki w Assets), (f) sterowanie głosem z Zadania 5 zamiast tilt („lewo", „prawo").
Zadanie 10, slajd 1/10
ZADANIE 10: Trener matematyczny v2 - wiele ekranów i ranking - cel
Cel zadania: rozbudować Trenera matematyczny (z prezentacji 4) o wiele ekranów, tryby trudności, pomiar czasu odpowiedzi oraz ranking TOP 3 w TinyDB - domknięcie kursu wieloekranową aplikacją.

Zakres v2 względem wersji z prezentacji 4

  • Wersja bazowa (prezentacja 4): jeden ekran, losowe działania, statystyka poprawnych/ błędnych.
  • v2 (to zadanie): menu główne → wybór trybu → ekran pytań → podsumowanie z czasem → ranking TOP 3 → powrót do menu.
  • Nowości: Screen2/Screen3 (wiele ekranów), zegar liczący czas reakcji, poziomy trudności (łatwy/średni/trudny), trwały ranking w TinyDB, przyciski nawigacyjne (Back).
  • Komponenty kluczowe: wiele Screen, Clock (pomiar), TinyDB (ranking), procedury (generacja pytań), List (wyniki).
Wiele ekranów to nowa złożoność: zmienne NIE SĄ współdzielone między ekranami domyślnie - każde Screen ma własną pamięć. Do przenoszenia danych służy metoda statyczna Screen2.HistoryList (wzorzec z prezentacji 4) lub globalny wzorzec TinyDB - oba omawiamy w slajdach 70-71.
Zadanie 10, slajd 2/10
Zadanie 10: mapa ekranów i przepływ nawigacji

Etap 1: plan ekranów

  1. Screen1 - Menu główne: tytuł, przyciski: ▶ Graj, 🏆 Ranking, ❌ Wyjście, wybór trybu (RadioButtons / ListPicker).
  2. Screen2 - Pytania: treść działania, 4 odpowiedzi (przyciski lub ListBox), licznik pytania, timer, pasek wyniku.
  3. Screen3 - Podsumowanie: wynik X/Y, średni czas, przyciski: Zapisz do rankingu, Powrót do menu.
  4. Screen4 - Ranking: lista TOP 3 z nazwami/graczy, przycisk Powrót. (opcjonalnie - można scalić z Screen3)

Etap 2: tworzenie dodatkowych ekranów

  1. W oknie Designer, listy rozwijanej ekranów (góra) kliknij Add Screen....
  2. Utwórz: Screen2, Screen3, Screen4 (kolejność alfabetyczna może się różnić od pożądanej - kontroluj nazwy).
  3. Dla każdego ustaw Title i AlignHorizontal = 3 dla spójności wizualnej.
  4. Wróć do Screen1, by budować menu.

Etap 3: przepływ (nawigacja)

Przepływ użytkownika: Screen1 (start, tryb) → open Screen2 → po 10 pytaniach open Screen3 → „Zapisz" zapis + open Screen4 → „Menu" open Screen1. Każdy ekran musi mieć wyjście - brak powrotu to pułapka martwego końca (dead end).
open screen to: blok z Control - przenosi użytkownika na inny ekran. Nie ma „stosu" jak w mobilnych frameworkach (Back by default) - przycisk Back trzeba implementować SAMODZIELNIE open screen to poprzedni ekran.
Zadanie 10, slajd 3/10
Zadanie 10: Screen1 - menu główne i wybór trybu

Etap 4: interfejs menu (Screen1)

  1. lblTytul: Text = 🧮 TRENER MATEMATYCZNY, FontSize = 26, FontBold, TextColor = Navy (lub ciemny granat), Width = Fill parent.
  2. lblPodtytul: Text = v2 - tryby, ranking, pomiar czasu, FontSize = 13, TextColor = Gray.
  3. lblTryb: Text = Poziom trudności:, FontSize = 15.
  4. RadioGroup1 lub trzy RadioButton: Łatwy (+-, do 20), Średni (×÷, do 100), Trudny (wszystkie, do 1000, z ułamkami). Nazwy: rbLatwy, rbSredni, rbTrudny.
  5. btnGraj: Text = ▶ GRAJ, BackgroundColor = Green, FontSize = 24, Width = Fill parent, Height = 70.
  6. btnRanking: Text = 🏆 RANKING TOP 3, BackgroundColor = Orange, Width = Fill parent, Height = 55.
  7. btnWyjscie: Text = ❌ WYJŚCIE, BackgroundColor = Red, Width = Fill parent, Height = 55.

Etap 5: zmienne trybu (na Screen1)

  1. Zmienna globalna wybranyTryb - string: "latwy" / "sredni" / "trudny".
  2. W zdarzeniach Click przycisków radiowych: set wybranyTryb to "latwy" itd. (lub odczyt z RadioGroup.Selection).
  3. Domyślnie w Initialize: set wybranyTryb to "sredni" + zaznacz rbSredni = true.
Walidacja wyjścia: btnWyjscie nie powinien zamykać od razu - użyj Notifier.ShowChooseDialog („Na pewno wyjść?"). Utrata niezapisanego progresu jednym kliknięciem to zły UX. Podobnie jak w Zadaniu 8 przy czyszczeniu bazy.
Zadanie 10, slajd 4/10
Zadanie 10: przekazywanie trybu między ekranami

Etap 6: zapis trybu i przejście na Screen2

  1. when btnGraj.Click do (na Screen1):
    • Walidacja: if żaden RadioButton nie wybrany → Notifier komunikat „Wybierz poziom trudności" i return.
    • call TinyDB1.StoreValue tag "tryb" valueToStore get global wybranyTryb (dodaj TinyDB1 do Screen1 - komponenty są per-screen!).
    • open screen to Screen2.

Etap 7: odczyt trybu na Screen2

  1. Dodaj TinyDB2 na Screen2 (osobna instancja!).
  2. when Screen2.Initialize do:
    • set global tryb to call TinyDB2.GetValue tag "tryb" blockIfTagNotThere "sredni".
    • Na tej podstawie ustaw zakresy pytań: if tryb = "latwy" then maxLiczba = 20, operacje = +,-; if "sredni" → 100, ×,÷; if "trudny" → 1000, wszystkie + trudniejsze typy.
    • Zainicjalizuj zmienne sesji: punkty=0, numerPytania=1, sumaCzasu=0.
    • call nowePytanie (procedura generująca pytanie).
    • Włącz set ClockSesji.TimerEnabled to true - start pomiaru czasu sesji / pytania.
Komponenty per-screen: TinyDB, Clock, Notifier itd. trzeba dodać OSOBNO na każdym ekranie, który ich używa - nie ma globalnej puli. Nazwy typu TinyDB1/TinyDB2 są tylko etykietami - pod spodem to niezależne instancje, ale wspólne dane (tagi) w tej samej bazie urządzenia - dlatego tag "tryb" widoczny z obu stron.
Zadanie 10, slajd 5/10
Zadanie 10: Screen2 - generowanie pytań i pomiar czasu

Etap 8: komponenty Screen2

  1. lblPytanie: Text = 7 × 8 = ?, FontSize = 40, FontBold, Width = Fill parent, Height = 90.
  2. Cztery przyciski odpowiedzi: btnOdp1 ... btnOdp4, FontSize = 22, Height = 60, Width = 45% (układ 2×2 - dwa w wierszu).
  3. lblPostep: Text = Pytanie 1/10, FontSize = 15, TextColor = Gray.
  4. lblWynikZywo: Text = Punkty: 0 | Czas: 0.0s, FontSize = 15.
  5. Clock1: TimerInterval = 100 (10 razy na sekundę - do odliczania wyświetlania; pomiar dokładny poniżej).
  6. Notifier1 do krótkich komunikatów (poprawnie/błędnie).

Etap 9: procedura nowePytanie

  1. Losuj a, b, op (1..4 operacje dozwolone w trybie).
  2. Oblicz poprawnyWynik (uwaga: dzielenie tylko gdy b dzieli a dokładnie - warunek a mod b = 0 lub losuj a jako b×k).
  3. Wygeneruj 4 odpowiedzi: poprawna + 3 dystraktory (wynik±losowaMalaWartosc, wynik×2, przekręcona cyfra - deterministyczne dystraktory budują myślenie, nie czysty random).
  4. Wymieszaj odpowiedzi: shuffle list (Lists) i zapamiętaj indeks poprawnej PO tasowaniu (lub szukaj wartości poprawnej przy kliknięciu).
  5. Ustaw lblPytanie, Texty 4 przycisków, lblPostep.
  6. set global startPytania to call Clock1.SystemTime (ms epoch - patrz niżej) lub użyj zmiennego licznika ticków Clock1.
Pomiar czasu: App Inventor Clock ma metodę SystemTime zwracającą czas w milisekundach od epoki. Zapisuj startPytania = SystemTime przy nowym pytaniu, a przy odpowiedzi: czas = (SystemTime - startPytania) / 1000 (sekundy z przecinkiem). To dokładniejsze niż liczenie ticków TimerInterval.
Zadanie 10, slajd 6/10
Zadanie 10: ocena odpowiedzi i przepływ pytań

Etap 10: zdarzenia kliknięcia odpowiedzi

  1. Dla każdego btnOdpN: when btnOdpN.Click do → call ocenaOdpowiedzi(N) (procedura z parametrem - unikamy duplikacji 4× tej samej logiki).

Etap 11: procedura ocenaOdpowiedzi(indeksKliknietego)

  1. Zablokuj dalsze kliknięcia: set global odpowiedzZablokowana to true (do czasu nowego pytania).
  2. Policz czas: set czasOdpowiedzi to (Clock1.SystemTime - global startPytania) / 1000.
  3. if kliknietyIndeks = global poprawnyIndeks then:
    • set punkty to punkty + 10 (podstawowe) + bonus szybkości: if czas < 2s then +5 - motywuje do plynności.
    • Pokoloruj btnOdpN.BackgroundColor = Green (feedback wizualny).
    • Notifier / lbl krótka pochwała.
    else: pokoloruj czerwonym TO zielone (poprawna) + czerwonym kliknięte - natychmiastowa korekta dydaktyczna.
  4. set sumaCzasu to sumaCzasu + czasOdpowiedzi, set sumaPunktow to ....
  5. set numerPytania to numerPytania + 1.
  6. if numerPytania > 10 then call koniecSesji; else wait 1.2 s (Clock opóźnienia / Clock2.Timer one-shot) → reset kolorów, odpowiedzZablokowana=false, call nowePytanie.

Etap 12: zliczanie i warunek końca

  1. Zmienne: punkty=0, numerPytania=1, sumaCzasu=0, poprawne=0, bledne=0, odpowiedzZablokowana=false.
  2. Pętla for nie jest tu potrzebna - pytania są SEKWENCYJNE, sterowane stanem numerPytania, nie iteracją listy.
Blokada wielokrotnego kliknięcia: bez odpowiedzZablokowana gracz może kliknąć 2 przyciski w 50 ms między oceną a nowym pytaniem - podwójna punktacja lub stan korupcyjny. Blokada + jasne jej zwolnienie (przy nowym pytaniu) to wzorzec idempotentnej obsługi wejścia.
Zadanie 10, slajd 7/10
Zadanie 10: Screen3 - podsumowanie sesji

Etap 13: interfejs podsumowania

  1. lblNaglowek: Text = 📊 WYNIK SESJI, FontSize = 26, FontBold, Width = Fill parent.
  2. lblWynikDetails: Text (wypełniane w Initialize), FontSize = 18, MultiLine, Width = Fill parent, Height = 150. Treść np.: Poprawne: 8/10 Punkty: 95 Średni czas: 3.4 s Tryb: Średni
  3. lblOcena: FontSize = 22, FontBold, Height = 60 - dynamiczna ocena procentowa / literowa.
  4. btnZapisz: Text = 💾 Zapisz do rankingu, BackgroundColor = Green, Width = Fill parent, Height = 60.
  5. btnPowrot: Text = 🏠 Powrót do menu, BackgroundColor = Gray, Width = Fill parent, Height = 55.
  6. TinyDB3, Notifier1 na tym ekranie.

Etap 14: Screen3.Initialize - odczyt wyników sesji

  1. Dane sesji NIE są współdzielone między Screen! Zapisz je PRZED open screen to Screen3:
    • Na Screen2 w koniecSesji: TinyDB.StoreValue "ostatniWynik" = lista [punkty, poprawne, bledy, sredniCzas, tryb].
    • open screen to Screen3.
  2. W Screen3.Initialize: wynik = GetValue "ostatniWynik" (pusta lista) → rozpakuj 5 pól → ustaw etykiety.
  3. Oblicz ocenę: if procent ≥ 90 → „🏆 BRAWO!", ≥ 70 → „👍 DOBRZE", ≥ 50 → „OK - ćwicz dalej", else → „📚 Potrzebujesz powtórki".
KoniecSesji (na Screen2): zatrzymaj Clock, policz średni czas (sumaCzasu/10), zapisz pakiet wyniku do TinyDB, open screen to Screen3. Jeden przycisk w Script - cała logika zamknięta w procedurze koniecSesji wywoływanej z warunku numerPytania > 10.
Zadanie 10, slajd 8/10
Zadanie 10: ranking TOP 3 w TinyDB

Etap 15: struktura rankingu

Ranking to lista do 3 elementów, każdy to lista [punkty, nazwaGracza, tryb, data]. Klucz TinyDB: ranking. Sortowanie malejąco po punktach, przycinanie do 3 przy wstawianiu.
  1. Zmienne Screen4 (lub Screen3 jeśli scalone): global top3 = empty list.
  2. W Initialize: set top3 to GetValue "ranking" (empty list).

Etap 16: wstawianie nowego wyniku (na Screen3 po kliknięciu btnZapisz)

  1. Walidacja: if wynik już zapisany w tej sesji (flaga) → komunikat i return - bez dublowania.
  2. Pobierz / wpisz nazwę: TextBox txtNazwa na Screen3 (Hint = "Twoje imię...", maxLength = 12). if puste → Notifier „Podaj nazwę gracza".
  3. set nowyRekord to create list (get global punkty, get txtNazwa.Text, get global tryb, call Clock.DatetimeToString).
  4. Dołącz do rankingu: set rankingLista to GetValue "ranking" → add items to list rankingLista, item nowyRekord.
  5. Sortuj malejąco po elemencie 1 (punkty): App Inventor NIE ma wbudowanego sort - zaimplementuj procedurę sortujRanking metodą bąbelkową na max 4 elementach (wydajność nieistotna przy n=4):
    • pętla for i od 1 do length-1, for j od 1 do length-i:
    • if select(ranking, j)[1] < select(ranking, j+1)[1] → zamień (swap przez tymczasową listę).
  6. Przytnij do 3: while length > 3 → remove last (lub buduj nową listę z pierwszych 3).
  7. StoreValue "ranking" rankingLista.
  8. Pokazuj listę: open screen to Screen4 lub odśwież lblRanking na Screen3.
Brak wbudowanego sort to częste ograniczenie AI2. Dla małych list (TOP 3, TOP 10) bąbelek lub selection sort w 15 blokach są wystarczające. Alternatywa: utrzymuj ranking zawsze posortowany wstawiając nowy element na właściwe miejsce (insert with shift) - operacja O(n), przy n=3 trywialna.
Zadanie 10, slajd 9/10
Zadanie 10: Screen4 - wyświetlanie rankingu

Etap 17: interfejs rankingu (Screen4)

  1. lblTytulRank: Text = 🏆 RANKING TOP 3, FontSize = 24, FontBold, Width = Fill parent.
  2. lstRanking (ListView): Width = Fill parent, Height = 220 - wyświetlane wiersze.
  3. btnPowrotRank: Text = ← Powrót, BackgroundColor = Gray, Width = Fill parent.
  4. TinyDB4 na tym ekranie.

Etap 18: procedura wyswietlRanking

  1. W Initialize: set top3 to GetValue "ranking" (empty list).
  2. if length(top3) = 0 → lstRanking.Elements = ["Brak wyników - zagraj pierwszą sesję!"] i return.
  3. for i od 1 do length(top3):
    • rekord = select(top3, i).
    • medal = if i=1 "🥇" else if i=2 "🥈" else "🥉".
    • tekst = join(medal " " select(rekord,2) " - " select(rekord,1) " pkt (" select(rekord,3) ")".
    • add items to list (etykiety, tekst).
  4. set lstRanking.Elements to etykiety.

Etap 19: przyciski powrotu - nawigacja wstecz

  1. btnPowrotRank.Click → open screen to Screen1.
  2. Systemowy Back Androida: w Screen2/3/4 dodaj zdarzenie BackPressed (jeśli dostępne w AI2) lub obsłuż w Screen1 - warto mapować Back na powrót do Screen1 zamiast zamykania aplikacji, gdy użytkownik jest głęboko w nawigacji.
Pusta baza: zawsze obsługuj length=0 - inaczej for od 1 do 0 nie doda nic, a użytkownik zobaczy czarny ekran listy i pomyśli, że ranking zepsuty. Komunikat „zagraj pierwszą sesję" zamienia pustkę w zachętę do akcji.
Zadanie 10, slajd 10/10
Zadanie 10: testowanie Trenera v2 i podsumowanie

Etap 20: testy funkcjonalne - checklista

Test 1 (menu): Bez wyboru trybu klik GRAJ → komunikat, brak przejścia.
Test 2 (przepływ): Wybierz Średni → GRAJ → Screen2 z poprawnymi zakresami pytań (×,÷, do 100).
Test 3 (pętla 10 pytań): Odpowiedz na wszystkie → dokładnie 10 ekranów pytań → Screen3.
Test 4 (ocena): 10 poprawnych → 100 pkt (+bonusy), ocena „🏆 BRAWO!".
Test 5 (korekta): Błędna → czerwony + podświetlenie poprawnej, punkt nie naliczany.
Test 6 (czas): Średni czas rośnie przy celowym zwalnianiu; bonus za <2s działa.
Test 7 (blokada): Szybkie dwukrotne kliknięcie NIE daje +20.
Test 8 (ranking): Zapisz nazwą → Screen4 pokazuje 🥇 na górze po sortowaniu.
Test 9 (TOP 3): Zagraj 5 sesji z różnymi wynikami → w rankingu tylko 3 najlepsze, reszta odrzucona.
Test 10 (trwałość): Zamknij aplikację → otwórz → Ranking jak przed zamknięciem (TinyDB).
Test 11 (tryby): Powtórz sesję w Łatwym i Trudnym → zakresy pytań i tryb w rankingu się zgadzają.
Test 12 (nawigacja): Back / przyciski → nigdzie nie utkniesz (brak dead-end).

Lista kontrolna

  • ✓ Cztery ekrany tworzą spójną pętlę nawigacyjną
  • ✓ Tryb przechodzi TinyDB z Screen1 do Screen2 (i do rankingu)
  • ✓ Dokładnie 10 pytań na sesję, blokada podwójnego kliknięcia
  • ✓ Pomiar czasu przez SystemTime, średni czas w podsumowaniu
  • ✓ Ranking sortowany, przycinany do 3, trwały między sesjami
  • ✓ Puste stany (brak rankingu, brak trybu) obsługiwane komunikatem

Czego uczy to zadanie (i cały arkusz)

  1. Architektura wieloekranowa - separacja odpowiedzialności, przepływ, brak dead-endów.
  2. Współdzielenie stanu przez TinyDB jako „czarną skrzynkę" między Screen.
  3. Generowanie danych + pętla stanowa (numerPytania) zamiast sztywnej iteracji.
  4. Metryki wydajnościowe (SystemTime) i mechaniki motywacyjne (bonus, ranking, medale).
  5. Sortowanie małe-n (bąbelek) gdy brak gotowej biblioteki.
  6. CRUD rankingu w praktyce: wstawianie, odczyt, przycinanie, trwałość.
Rozszerzenia: (a) tryb nauki - bez punktów, tylko powtórka błędnych pytań, (b) wykres postępu w czasie (WebViewer + statyki SVG/Canvas), (c) tryb dla 2 graczy naprzemiennie na jednym urządzeniu, (d) import/eksport rankingu (Share / Web), (b) tryb offline vs online (WebJSON) do porównywania z klasą.
Podsumowanie, slajd 1/1
Podsumowanie arkusza - mapa umiejętności

Co ćwiczy każde zadanie

  • Zadanie 1 (Powitanie): zdarzenia Click, blok join, TextBox, walidacja pustego pola, zmiana kolorów.
  • Zadanie 2 (Kalkulator): zmienne, ListPicker, if/else, Notifier, dzielenie przez zero, formatowanie wyniku.
  • Zadanie 3 (Quiz): listy, indeksowanie, liczniki, punktacja, pętle po danych, podsumowanie sesji.
  • Zadanie 4 (Kolory): BackgroundColor, random integer, select list item, obsługa listy stałych.
  • Zadanie 5 (Mowa): TextToSpeech.Speak, SpeechRecognizer.GetText / AfterGettingText, porównania tekstów, downcase.
  • Zadanie 6 (Kostka): AccelerometerSensor.Shaking, procedury, losowanie, kalibracja Threshold.
  • Zadanie 7 (Poziomica): AccelerationChanged, atan → stopnie, if/else if z progami, wartość bezwzględna.
  • Zadanie 8 (Notatnik): Camera.AfterPicture, LocationSensor, TinyDB, listy zagnieżdżone, CRUD, pętle for.
  • Zadanie 9 (Gra): Canvas, Ball/Sprite, Clock jako pętla gry, CollidedWith, maszyna stanów, tilt control, rekord w TinyDB.
  • Zadanie 10 (Trener v2): wiele ekranów, open screen to, współdzielenie stanu, SystemTime, sortowanie n, ranking TOP 3, pełna pętla UX.
Zasada przenośności: żadne zadanie nie jest izolowane - mechanizmy wracają w kolejnych (procedury → kostka → wszystko; TinyDB → notatnik → rekord gry → ranking). Kurs jest zaprojektowany jako siatka powiązań, nie lista 10 osobnych ćwiczeń.

Materiały i dalsza nauka

  • Prezentacje 1-5 i ćwiczenie ex1 (Kalkulator) na pam.netstudio.net.pl.
  • Dokumentacja MIT App Inventor: ai2.appinventor.mit.edu (Help → Documentation).
  • Forum społeczności: community.appinventor.mit.edu - zanim zapytasz, przeszukaj - 99% problemów zostało już rozwiązanych.
  • appinventor.org - materiały i tutoriale dodatkowe.