Przejdź do treści
INF.04 · Dział V

Tworzenie aplikacji (Desktop i Mobile)

Projektowanie interfejsów graficznych (GUI), cykl życia aplikacji mobilnej, obsługa zdarzeń UI, łączenie z zewnętrznym API.

Wstęp: tu egzamin sprawdza, czy rozumiesz, kiedy co się dzieje

Aplikacja z interfejsem graficznym różni się od programu konsolowego jedną rzeczą: nie ty decydujesz o kolejności wykonania, tylko użytkownik. Program czeka, aż ktoś kliknie, obróci telefon albo odbierze połączenie — i musi poprawnie zareagować na każdy z tych przypadków. Dlatego zadania z tego działu prawie zawsze pytają o kolejność: która metoda cyklu życia wykona się po obróceniu ekranu, co się stanie, gdy zapytanie do serwera puścisz w wątku interfejsu, który kod trafi do widoku, a który do modelu. Nazwy metod, kody odpowiedzi HTTP i nazwy kontrolek trzeba znać z pamięci.

1

Rodzaje aplikacji i ich porównanie

Typowe zadanie brzmi: „klient chce aplikację do skanowania kodów kreskowych, działającą offline, na telefonach z Androidem i iOS, budżet ograniczony — zaproponuj rodzaj aplikacji i uzasadnij". Żeby na to odpowiedzieć, musisz umieć porównać pięć wariantów po tych samych kryteriach.

Desktopowa

Instalowana w systemie operacyjnym, uruchamiana lokalnie. Pełny dostęp do plików, portów, drukarek i sprzętu. Działa bez internetu, ale aktualizację trzeba dostarczyć na każdy komputer osobno. Przykłady technologii: C# WinForms i WPF, Java Swing i JavaFX, Python z Tkinter lub Qt, Electron.

Webowa

Uruchamiana w przeglądarce, kod wykonuje się częściowo na serwerze, częściowo u klienta. Nie wymaga instalacji, aktualizacja jest natychmiastowa dla wszystkich. Dostęp do sprzętu tylko przez API przeglądarki i po zgodzie użytkownika. Technologie: HTML, CSS, JavaScript, React, PHP, ASP.NET.

Mobilna natywna

Pisana pod konkretny system w jego własnym SDK: Android — Kotlin lub Java w Android Studio, iOS — Swift w Xcode. Najlepsza wydajność i pełny dostęp do czujników, ale każdą platformę robisz od zera, więc koszt jest podwójny.

Hybrydowa i wieloplatformowa

Jeden kod źródłowy, wiele platform. Aplikacja hybrydowa w ścisłym sensie to strona uruchomiona w komponencie WebView i opakowana w plik instalacyjny (Apache Cordova, Ionic). Frameworki wieloplatformowe (Flutter, React Native, .NET MAUI) rysują własny interfejs lub korzystają z natywnych kontrolek.

PWA — Progressive Web App

Aplikacja webowa, którą można „zainstalować" na pulpicie albo ekranie głównym telefonu i która działa też bez sieci. Trzy elementy, o które pyta egzamin: manifest (plik JSON z nazwą, ikonami i trybem wyświetlania), service worker (skrypt pośredniczący w żądaniach sieciowych, odpowiada za pamięć podręczną i pracę offline) oraz wymóg działania po HTTPS. PWA nie trafia do sklepu z aplikacjami — instaluje się prosto z przeglądarki.

Porównanie typów aplikacji
TypDostęp do sprzętuWydajnośćKoszt wytworzeniaDystrybucjaTechnologie
Desktopowapełny (pliki, porty, drukarki)bardzo wysokaśredni, rośnie z liczbą systemówinstalator EXE, MSI, DEBWPF, JavaFX, Qt
Webowaograniczony, przez API przeglądarkiśrednia, zależna od łączaniski — jeden kod na wszystkoadres URL, bez instalacjiHTML, CSS, JS, PHP
Mobilna natywnapełny (GPS, aparat, NFC, czujniki)najwyższa na danym systemiewysoki — osobny kod na systemGoogle Play, App StoreKotlin, Java, Swift
Hybrydowaprzez wtyczki pomostoweniższa niż natywnaniski — jeden kod na dwa systemysklepy z aplikacjamiCordova, Ionic
Wieloplatformowaszeroki, przez gotowe modułyzbliżona do natywnejśrednisklepy z aplikacjamiFlutter, React Native, MAUI
PWAograniczony, jak w przeglądarceśrednianajniższyinstalacja z przeglądarkiJS + service worker

Jak odpowiadać na zadanie „dobierz typ aplikacji"

Nie pisz „bo jest nowoczesna". Uzasadnienie punktuje się za powiązanie wymagania z cechą: praca offline i czujniki → natywna lub wieloplatformowa; jedna wersja dla wszystkich systemów i szybkie poprawki → webowa lub PWA; praca z lokalnymi plikami i urządzeniami podłączonymi po USB → desktopowa; mały budżet przy wymogu obecności w sklepie → hybrydowa lub wieloplatformowa.

2

Projektowanie interfejsu graficznego

Zadanie praktyczne zwykle podaje rysunek okna i listę wymagań, a ty masz zbudować interfejs i podpiąć zdarzenia. Punkty lecą za dobór właściwej kontrolki — pole wyboru tam, gdzie miał być przycisk opcji, to błąd merytoryczny, nie kosmetyczny.

Podstawowe kontrolki i ich zdarzenia
Nazwa polskaNazwa angielskaDo czego służyTypowe zdarzenie
EtykietaLabel / TextViewwyświetla tekst opisujący inną kontrolkę lub wynik; użytkownik jej nie edytujezwykle brak, opcjonalnie kliknięcie
Pole tekstoweTextBox / EditTextwprowadzanie tekstu lub liczby przez użytkownikazmiana tekstu, utrata fokusu
PrzyciskButtonuruchamia akcję: zapis, obliczenie, wysłanie formularzakliknięcie, długie naciśnięcie
Lista rozwijanaComboBox / Spinnerwybór jednej pozycji z wielu bez zajmowania miejsca na ekraniezmiana zaznaczonej pozycji
Pole wyboruCheckBoxniezależne włącz/wyłącz; można zaznaczyć wiele narazzmiana stanu zaznaczenia
Przycisk opcjiRadioButtonwybór dokładnie jednej opcji w grupie — zaznaczenie jednej odznacza pozostałezmiana zaznaczenia w grupie
Lista pozycjiListBox / RecyclerViewprezentacja zbioru rekordów, zwykle z przewijaniemkliknięcie pozycji listy
TabelaDataGridView / TableLayoutdane w wierszach i kolumnach, zwykle z bazy; sortowanie i edycja komórekzmiana wiersza, edycja komórki
MenuMenuStrip / Menugrupuje polecenia programu (Plik, Edycja, Pomoc) i skróty klawiszowewybór pozycji menu

Pole wyboru czy przycisk opcji?

Reguła jest prosta i pytana wprost: CheckBox — odpowiedzi niezależne, można zaznaczyć zero, jedną albo wszystkie („wybierz dodatki"). RadioButton — odpowiedzi wykluczające się, zawsze dokładnie jedna („sposób płatności", „forma dostawy"). Jeśli opcji wykluczających się jest więcej niż pięć albo sześć, zamiast przycisków opcji stosuje się listę rozwijaną — mniej miejsca, ta sama semantyka.

Opis interfejsu: układ ekranu w Androidzie

W aplikacji mobilnej wygląd zwykle opisuje się deklaratywnie w pliku XML, a kod zajmuje się tylko logiką. Każdej kontrolce nadaje się identyfikator, po którym kod ją odnajduje.

<LinearLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:padding="16dp">

  <TextView
      android:id="@+id/etykietaLogin"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Login uzytkownika" />

  <EditText
      android:id="@+id/poleLogin"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:inputType="text" />

  <CheckBox
      android:id="@+id/polePamietaj"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Zapamietaj mnie" />

  <Button
      android:id="@+id/przyciskZaloguj"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="Zaloguj" />

</LinearLayout>

Wartość match_parent oznacza „rozciągnij na całą szerokość rodzica", a wrap_content — „zajmij tyle, ile potrzeba na zawartość". Wymiary podaje się w dp (jednostka niezależna od gęstości ekranu), a rozmiary czcionek w sp, które dodatkowo skalują się z ustawieniami dostępności użytkownika.

Menedżery układu (layout managers)

Kontrolek nie rozmieszcza się na sztywno we współrzędnych pikseli, bo okno da się przeskalować, a telefony mają różne ekrany. Od tego są menedżery układu — obiekty, które same wyliczają pozycje i rozmiary kontrolek.

  • Układ liniowy (LinearLayout, StackPanel, BoxLayout) — kontrolki jedna za drugą, w pionie albo w poziomie.
  • Układ siatkowy (GridLayout, TableLayout) — wiersze i kolumny, idealny do formularzy „etykieta + pole".
  • Układ brzegowy (BorderLayout, DockPanel) — pięć obszarów: góra, dół, lewo, prawo, środek. Typowy dla okna z paskiem narzędzi i paskiem stanu.
  • Układ względny (RelativeLayout, ConstraintLayout) — pozycję określa się przez relacje: „pod polem tekstowym", „wyrównaj do prawej krawędzi rodzica". Najelastyczniejszy i zalecany w Androidzie.
  • Kotwiczenie i dokowanie (właściwości Anchor i Dock w WinForms) — kontrolka trzyma się wybranych krawędzi okna przy zmianie jego rozmiaru.
Zasady projektowania interfejsu
  • Spójność — ta sama czynność wygląda i nazywa się tak samo na każdym ekranie. Przycisk zatwierdzający zawsze po tej samej stronie, ikona zapisu zawsze ta sama, konsekwentne nazewnictwo (albo wszędzie „Usuń", albo wszędzie „Skasuj").
  • Czytelność — wystarczający kontrast tekstu do tła, czcionka odpowiedniej wielkości, grupowanie powiązanych pól, odstępy zamiast ramek wszędzie. Na ekranie dotykowym obszar klikalny nie mniejszy niż mniej więcej 48 dp, żeby dało się trafić palcem.
  • Informacja zwrotna — użytkownik musi wiedzieć, że system przyjął jego akcję: przycisk zmienia wygląd po naciśnięciu, przy długiej operacji pojawia się pasek postępu, po zapisie krótki komunikat. Milczący interfejs użytkownik klika drugi raz i tworzy duplikat rekordu.
  • Zapobieganie błędom — lepiej nie dopuścić do błędu, niż go potem zgłaszać. Data wybierana z kalendarza zamiast wpisywana ręcznie, pole numeryczne z klawiaturą numeryczną, przycisk „Zapisz" nieaktywny, dopóki formularz jest niekompletny, pytanie o potwierdzenie przed nieodwracalnym usunięciem i możliwość cofnięcia operacji.
  • Czytelne komunikaty błędów — komunikat mówi, co jest źle i co zrobić („PESEL musi mieć 11 cyfr"), a nie „Error 0x8004".
  • Dostępność — opisy tekstowe kontrolek dla czytników ekranu (w Androidzie atrybut contentDescription), obsługa całego interfejsu z klawiatury, sensowna kolejność przechodzenia tabulatorem, brak przekazywania informacji wyłącznie kolorem — dodaj ikonę albo podpis, bo osoba z zaburzeniem widzenia barw koloru nie odróżni.
  • Responsywność — układ dopasowuje się do rozmiaru i orientacji ekranu. W Androidzie służą do tego osobne katalogi zasobów (na przykład layout-land dla orientacji poziomej i warianty według szerokości ekranu), w aplikacjach webowych — zapytania medialne CSS i siatki elastyczne.

3

Programowanie zdarzeniowe i wątek interfejsu

Program konsolowy wykonuje instrukcje po kolei i się kończy. Aplikacja okienkowa po zbudowaniu interfejsu wchodzi w nieskończoną pętlę zdarzeń i czeka. To odwrócenie sterowania: to system woła twój kod, a nie odwrotnie.

Pętla zdarzeń w pseudokodzie
zbuduj okno i kontrolki
zarejestruj procedury obslugi zdarzen

dopoki aplikacja dziala:
  zdarzenie = pobierz_z_kolejki()      // klik, klawisz, dotyk, timer
  jesli zdarzenie == ZAMKNIJ_OKNO:
      przerwij petle
  procedura = znajdz_obsluge(zdarzenie.zrodlo, zdarzenie.typ)
  jesli procedura istnieje:
      procedura(zdarzenie)             // TU wykonuje sie TWOJ kod
  odrysuj to, co sie zmienilo

zwolnij zasoby i zakoncz proces

Zapamiętaj trzy pojęcia: zdarzenie (fakt, że coś zaszło), kolejka zdarzeń (bufor, do którego system wrzuca zdarzenia w kolejności wystąpienia) i procedura obsługi zdarzenia (ang. event handler, metoda wywoływana w reakcji na zdarzenie). Powiązanie kontrolki z procedurą to subskrypcja, inaczej rejestracja nasłuchiwacza (listener).

Procedura obsługi zdarzenia — trzy zapisy tego samego
// C# / WinForms - podpiecie metody do zdarzenia Click
przyciskZaloguj.Click += PrzyciskZaloguj_Click;

private void PrzyciskZaloguj_Click(object sender, EventArgs e)
{
  if (poleLogin.Text.Trim() == "")
  {
      MessageBox.Show("Podaj login.");
      return;
  }
  etykietaStatus.Text = "Zalogowano jako " + poleLogin.Text;
}

// Java / Android - nasluchiwacz jako klasa anonimowa
przyciskZaloguj.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
      String login = poleLogin.getText().toString().trim();
      if (login.isEmpty()) {
          Toast.makeText(kontekst, "Podaj login", Toast.LENGTH_SHORT).show();
          return;
      }
      etykietaStatus.setText("Zalogowano jako " + login);
  }
});

// JavaScript - ten sam wzorzec w aplikacji webowej
document.getElementById("przyciskZaloguj")
      .addEventListener("click", function (e) {
          e.preventDefault();       // wstrzymuje domyslna akcje przegladarki
          console.log("Kliknieto", e.target.id);
      });

Parametr sender (w Javie View v) wskazuje kontrolkę, która zdarzenie wywołała — dzięki temu jedna procedura może obsłużyć kilka przycisków. Drugi parametr niesie szczegóły zdarzenia: współrzędne kliknięcia, kod klawisza, użyty przycisk myszy.

Zdarzenia myszy i dotyku
  • Click / onClick — pojedyncze kliknięcie lub dotknięcie.
  • DoubleClick — dwukrotne kliknięcie.
  • MouseEnter / MouseLeave — kursor wchodzi i opuszcza obszar kontrolki.
  • LongClick — długie przytrzymanie, typowo menu kontekstowe na telefonie.
  • Swipe, Drag — przesunięcie palcem, przeciąganie elementu.
Zdarzenia klawiatury, fokusu i okna
  • KeyDown / KeyPress / KeyUp — naciśnięcie i zwolnienie klawisza.
  • TextChanged — zmiana zawartości pola tekstowego; dobre miejsce na walidację na bieżąco.
  • GotFocus / LostFocus — kontrolka otrzymuje lub traci aktywność.
  • SelectedIndexChanged — zmiana pozycji w liście rozwijanej.
  • Load, Closing, Resize — załadowanie okna, próba zamknięcia, zmiana rozmiaru.
Wątek interfejsu — dlaczego aplikacja „zamarza"

Pętla zdarzeń działa w jednym, wyróżnionym wątku: wątku interfejsu (UI thread, w Androidzie zwanym też wątkiem głównym). Ten sam wątek pobiera zdarzenia z kolejki, wykonuje twoje procedury obsługi i rysuje kontrolki. Skoro robi to wszystko po kolei, to gdy twoja procedura liczy coś przez trzy sekundy, przez te trzy sekundy nikt nie pobiera zdarzeń ani nie odrysowuje okna — aplikacja wygląda na zawieszoną, a system może pokazać komunikat o braku odpowiedzi. W Androidzie taki stan to ANR (Application Not Responding) i pojawia się, gdy wątek główny pozostaje zablokowany przez kilka sekund.

// ZLE - cala robota w watku interfejsu
private void PrzyciskPobierz_Click(object sender, EventArgs e)
{
  string json = KlientHttp.Pobierz("https://api.szkola.pl/uczniowie");
  // okno jest zamrozone az do konca pobierania:
  // nie reaguje na klikniecia, nie odswieza sie, kursor "myslacy"
  listaUczniow.DataSource = Parsuj(json);
}

// DOBRZE - operacja poza watkiem interfejsu, wynik wraca do niego
private async void PrzyciskPobierz_Click(object sender, EventArgs e)
{
  przyciskPobierz.Enabled = false;      // blokada podwojnego klikniecia
  pasekPostepu.Visible = true;          // informacja zwrotna dla uzytkownika
  try
  {
      string json = await KlientHttp.PobierzAsync("https://api.szkola.pl/uczniowie");
      listaUczniow.DataSource = Parsuj(json);   // jestesmy juz z powrotem w UI
  }
  catch (Exception ex)
  {
      MessageBox.Show("Blad pobierania: " + ex.Message);
  }
  finally
  {
      pasekPostepu.Visible = false;
      przyciskPobierz.Enabled = true;
  }
}

Zasada, którą trzeba umieć wypowiedzieć na egzaminie: operacje długotrwałe — sieć, odczyt dużego pliku, zapytanie do bazy, złożone obliczenia — wykonujemy w wątku roboczym (w tle), a interfejs aktualizujemy wyłącznie z wątku interfejsu. Odwrotny błąd też jest karany: próba zmiany kontrolki bezpośrednio z wątku roboczego kończy się wyjątkiem. Mechanizmy warte zapamiętania: wątki i pule wątków, async/await w C#, korutyny w Kotlinie, klasa AsyncTask w starszym Androidzie oraz metody typu Invoke i runOnUiThread, którymi wraca się na wątek interfejsu.

4

Warstwy aplikacji, MVC i MVVM

Cała logika wpisana w procedurę obsługi kliknięcia to najczęstszy błąd projektowy początkującego. Podział na warstwy oznacza, że każdy fragment kodu ma jedną odpowiedzialność, a warstwa wyższa korzysta z niższej, nigdy odwrotnie.

Warstwa prezentacji

Okna, ekrany, kontrolki, procedury obsługi zdarzeń. Odbiera dane od użytkownika, sprawdza je pobieżnie i wyświetla wyniki. Nie zna SQL-a ani reguł biznesowych.

Warstwa logiki

Reguły działania programu: obliczenia, walidacja merytoryczna, uprawnienia, przepływ operacji. Tę warstwę da się przetestować bez uruchamiania okna i to jest jej główna zaleta.

Warstwa danych

Dostęp do bazy, plików i zewnętrznego API. Zapytania SQL, mapowanie rekordów na obiekty. Zmiana bazy z SQLite na MySQL powinna dotknąć wyłącznie tej warstwy.

MVC — Model, Widok, Kontroler
  • Model — dane i reguły ich przetwarzania; nie wie nic o wyglądzie.
  • Widok (View) — prezentacja danych modelu użytkownikowi.
  • Kontroler (Controller) — przyjmuje akcje użytkownika, zmienia model i wybiera widok do pokazania.
/aplikacja
  /model
      Uczen.cs                  klasa danych: imie, nazwisko, klasa, srednia
      RepozytoriumUczniow.cs    odczyt i zapis do bazy (warstwa danych)
  /widok
      OknoListyUczniow.xaml     tylko uklad kontrolek
      OknoEdycji.xaml
  /kontroler
      KontrolerUczniow.cs       obsluga akcji: dodaj, usun, zapisz

// Kontroler laczy jedno z drugim:
public void DodajUcznia(string imie, string nazwisko, double srednia)
{
  if (srednia < 1.0 || srednia > 6.0)                     // regula logiki
      throw new ArgumentException("Srednia poza zakresem 1-6");

  Uczen u = new Uczen(imie, nazwisko, srednia);            // model
  repozytorium.Zapisz(u);                                  // warstwa danych
  widok.OdswiezListe(repozytorium.PobierzWszystkich());    // prezentacja
}
MVVM

Model — Widok — ViewModel. Zamiast kontrolera jest ViewModel, czyli obiekt wystawiający dane w postaci gotowej do wyświetlenia. Widok łączy się z nim przez wiązanie danych (data binding): zmiana właściwości w ViewModelu automatycznie odświeża kontrolkę i odwrotnie. Stosowany w WPF, .NET MAUI i nowoczesnym Androidzie.

Co daje podział
  • Ten sam model obsłuży wersję desktopową i mobilną.
  • Logikę da się przetestować testami jednostkowymi.
  • Nad projektem może pracować kilka osób równolegle.
  • Zmiana wyglądu nie grozi zepsuciem obliczeń.
  • W MVVM ViewModel przeżywa obrót ekranu, więc dane nie znikają.

5

Cykl życia aktywności w Androidzie

Aktywność (Activity) to pojedynczy ekran aplikacji. Nie ty decydujesz, kiedy powstaje i ginie — robi to system, bo w każdej chwili może przyjść telefon, użytkownik może przełączyć aplikację albo obrócić ekran. Twoim zadaniem jest wypełnić właściwe metody właściwym kodem. Ta lista metod i ich kolejność to najczęściej pytany fragment całego działu.

Diagram cyklu życia aktywności
                    uruchomienie aplikacji
                           |
                           v
                     +-----------+
                     | onCreate  |  utworzenie aktywnosci
                     +-----------+
                           |
                           v
                     +-----------+ <--------------------+
                     | onStart   |  ekran staje sie      |
                     +-----------+  widoczny             |
                           |                             |
                           v                             |
                     +-----------+ <-------+       +-----------+
                     | onResume  |         |       | onRestart |
                     +-----------+         |       +-----------+
                           |               |             ^
                 AKTYWNOSC NA WIERZCHU     |             |
                 (uzytkownik pracuje)      |             |
                           |               |             |
                           v               |             |
                     +-----------+         |             |
                     | onPause   | --------+             |
                     +-----------+  powrot z okna        |
                           |        dialogowego          |
                           v                             |
                     +-----------+                       |
                     | onStop    | ----------------------+
                     +-----------+  powrot do aplikacji
                           |
                           v
                     +-----------+
                     | onDestroy |  zwolnienie zasobow
                     +-----------+
                           |
                           v
                    koniec aktywnosci


Scenariusze do zapamietania:

start aplikacji ......... onCreate -> onStart -> onResume
czesciowe zaslonienie ... onPause                  (np. okno dialogowe)
przejscie w tlo ......... onPause -> onStop
powrot z tla ............ onRestart -> onStart -> onResume
zamkniecie ekranu ....... onPause -> onStop -> onDestroy
obrot ekranu ............ onPause -> onStop -> onDestroy ->
                          onCreate -> onStart -> onResume
Metoda, moment wywołania i zawartość
MetodaKiedy jest wywoływanaCo w niej robić
onCreate()raz, przy tworzeniu aktywnościustawić układ (setContentView), odnaleźć kontrolki, podpiąć zdarzenia, odtworzyć stan z parametru savedInstanceState
onStart()gdy aktywność staje się widocznazarejestrować odbiorniki zdarzeń systemowych, rozpocząć obserwację danych
onResume()gdy aktywność trafia na wierzch i przyjmuje dotykwznowić animacje, kamerę, odtwarzanie, nasłuch GPS — wszystko, co ma działać tylko przy widocznym ekranie
onPause()gdy aktywność traci pierwszy plan (okno dialogowe, połączenie przychodzące)wstrzymać animacje i kamerę, zwolnić GPS; kod ma być krótki, bo opóźnia pokazanie następnego ekranu
onStop()gdy aktywność przestaje być widocznazapisać dane trwale (baza, preferencje), zwolnić cięższe zasoby, wyrejestrować odbiorniki
onRestart()przed onStart, gdy wracamy do zatrzymanej aktywnościodświeżyć dane, które mogły się zmienić, gdy aplikacja była w tle
onDestroy()przed zniszczeniem aktywności (zamknięcie, obrót ekranu, brak pamięci)zamknąć połączenia i pliki, zatrzymać wątki, zwolnić pamięć — nie zakładać, że zawsze się wykona
Zapis stanu przy obrocie ekranu

Obrót ekranu niszczy i tworzy aktywność od nowa, bo trzeba załadować układ dopasowany do nowej orientacji. Zmienne pola klasy giną razem z obiektem. Dlatego stan tymczasowy zapisuje się w metodzie onSaveInstanceState, a odtwarza w onCreate.

public class GlownaAktywnosc extends AppCompatActivity {

  private int licznik = 0;
  private TextView etykietaLicznik;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_glowna);

      etykietaLicznik = findViewById(R.id.etykietaLicznik);
      Button przycisk = findViewById(R.id.przyciskDodaj);

      // odtworzenie stanu po obrocie ekranu
      if (savedInstanceState != null) {
          licznik = savedInstanceState.getInt("licznik", 0);
      }
      etykietaLicznik.setText(String.valueOf(licznik));

      przycisk.setOnClickListener(v -> {
          licznik++;
          etykietaLicznik.setText(String.valueOf(licznik));
      });
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
      super.onSaveInstanceState(outState);
      outState.putInt("licznik", licznik);   // stan tymczasowy -> Bundle
  }

  @Override
  protected void onStop() {
      super.onStop();
      // dane, ktore musza przetrwac wylaczenie aplikacji -> zapis trwaly
      getSharedPreferences("ustawienia", MODE_PRIVATE)
          .edit()
          .putInt("ostatniLicznik", licznik)
          .apply();
  }
}

Rozróżnienie punktowane na egzaminie: onSaveInstanceState (obiekt Bundle) służy do stanu tymczasowego, przeżywającego obrót ekranu i chwilowe usunięcie aktywności z pamięci. Do danych, które mają przetrwać zamknięcie aplikacji, używa się zapisu trwałego: preferencji, pliku albo bazy — i robi się to najpóźniej w onStop.

6

Uprawnienia i dane przechowywane lokalnie

Aplikacja mobilna działa w piaskownicy: domyślnie widzi tylko własny katalog i nic poza nim. Dostęp do aparatu, lokalizacji czy kontaktów wymaga uprawnienia, a wybór miejsca na dane trzeba umieć uzasadnić.

Uprawnienia aplikacji mobilnej

Każde uprawnienie deklaruje się w pliku manifestu. Uprawnienia zwykłe (na przykład dostęp do internetu) system przyznaje sam przy instalacji. Uprawnienia niebezpieczne — dotyczące prywatności i sprzętu — wymagają dodatkowo zapytania użytkownika w czasie działania aplikacji i sprawdzenia jego odpowiedzi.

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

// Kod: sprawdzenie i prosba o uprawnienie niebezpieczne
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
      != PackageManager.PERMISSION_GRANTED) {
  ActivityCompat.requestPermissions(
      this,
      new String[]{Manifest.permission.CAMERA},
      KOD_ZADANIA_APARAT);
} else {
  uruchomAparat();
}

// Odpowiedz uzytkownika wraca do metody zwrotnej
@Override
public void onRequestPermissionsResult(int kod, String[] uprawnienia,
                                     int[] wyniki) {
  if (kod == KOD_ZADANIA_APARAT
          && wyniki.length > 0
          && wyniki[0] == PackageManager.PERMISSION_GRANTED) {
      uruchomAparat();
  } else {
      // aplikacja MUSI dzialac dalej mimo odmowy - tylko w wezszym zakresie
      pokazKomunikat("Bez zgody na aparat nie zeskanujesz kodu.");
  }
}

Dwie zasady, o które pytają zadania opisowe: proś o uprawnienie w momencie, gdy jest naprawdę potrzebne (a nie o wszystkie naraz przy starcie) i proś tylko o to, czego rzeczywiście używasz — to zasada minimalnych uprawnień. Odmowa nie może powodować zamknięcia aplikacji.

Preferencje

Pary klucz–wartość dla prostych ustawień: motyw, język, ostatni login, flaga „widziałem samouczek". W Androidzie SharedPreferences, na komputerze plik konfiguracyjny albo rejestr. Nie trzymamy tu haseł ani dużych zbiorów danych.

Pliki

Pamięć wewnętrzna (katalog prywatny aplikacji, kasowany razem z nią) albo zewnętrzna (współdzielona, wymaga uprawnień). Nadaje się do zdjęć, dokumentów, plików CSV, XML i JSON oraz dzienników zdarzeń.

Baza SQLite

Relacyjna baza w jednym pliku, bez osobnego serwera, wbudowana w system mobilny. Do danych strukturalnych, których jest dużo i po których trzeba wyszukiwać, sortować i filtrować zapytaniami SQL.

SQLite w praktyce
-- Utworzenie tabeli przy pierwszym uruchomieniu aplikacji
CREATE TABLE IF NOT EXISTS zadania (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  tresc    TEXT    NOT NULL,
  termin   TEXT,                 -- SQLite nie ma typu DATE, data jako TEXT
  wykonane INTEGER DEFAULT 0     -- ani typu BOOLEAN, 0/1 jako INTEGER
);

-- Typowe operacje CRUD
INSERT INTO zadania (tresc, termin) VALUES ('Powtorka INF.04', '2026-08-20');
SELECT id, tresc, termin FROM zadania WHERE wykonane = 0 ORDER BY termin;
UPDATE zadania SET wykonane = 1 WHERE id = 3;
DELETE FROM zadania WHERE id = 3;

Przy aktualizacji aplikacji zmieniającej strukturę tabel podnosi się numer wersji bazy; system wywołuje wtedy metodę migracji (w Androidzie onUpgrade), w której dopisujesz kolumny albo przenosisz dane. Zapytania z danymi od użytkownika zawsze buduj przez parametry, nigdy przez sklejanie łańcuchów znaków — inaczej otwierasz drogę do wstrzyknięcia SQL.

7

Komunikacja z zewnętrznym API i JSON

Aplikacja rzadko jest samotną wyspą — pobiera dane z serwera przez API udostępnione po HTTP. Schemat jest zawsze ten sam: klient wysyła żądanie z metodą i adresem, serwer odsyła kod statusu i treść, najczęściej w formacie JSON.

Metody HTTP
  • GET — pobranie zasobu. Nie zmienia danych, parametry przekazywane w adresie URL.
  • POST — utworzenie nowego zasobu. Dane w treści żądania, w odpowiedzi zwykle kod 201.
  • PUT — zastąpienie istniejącego zasobu nową wersją (PATCH zmienia tylko wybrane pola).
  • DELETE — usunięcie zasobu wskazanego adresem.

Zestaw GET/POST/PUT/DELETE odpowiada operacjom CRUD znanym z baz danych: odczyt, tworzenie, aktualizacja, usunięcie.

Elementy żądania
  • Adres URL — na przykład /api/uczniowie/12.
  • Nagłówki — Content-Type (format wysyłanych danych), Accept, Authorization (token dostępu).
  • Treść (body) — dane w formacie JSON przy POST i PUT; przy GET jej nie ma.
  • HTTPS — komunikacja szyfrowana; przesyłanie tokenów po zwykłym HTTP to błąd bezpieczeństwa.
Kody odpowiedzi HTTP
KodNazwaZnaczenieReakcja aplikacji
200OKżądanie wykonane, dane w treści odpowiedziprzetwarza JSON i wyświetla wynik
201Createdzasób został utworzony — typowa odpowiedź na POSTczyści formularz, odświeża listę
400Bad Requestbłędne żądanie: brak pola, zły format danychpokazuje błąd walidacji przy polu formularza
401Unauthorizedbrak uwierzytelnienia albo token wygasłprzenosi na ekran logowania
403Forbiddenużytkownik jest rozpoznany, ale nie ma uprawnieńinformuje o braku dostępu
404Not Foundnie ma zasobu pod tym adresem (zły identyfikator, literówka w URL)komunikat „nie znaleziono"
500Internal Server Errorbłąd po stronie serwera — klient nic nie zawiniłproponuje ponowienie próby później

Klucz do zapamiętania całych klas kodów: 2xx — sukces, 3xx — przekierowanie, 4xx — wina klienta, 5xx — wina serwera.

Przykładowa odpowiedź API w formacie JSON
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"status": "ok",
"liczbaWynikow": 3,
"uczniowie": [
  {
    "id": 12,
    "imie": "Anna",
    "nazwisko": "Kowalska",
    "klasa": "3A",
    "srednia": 4.75,
    "aktywny": true,
    "przedmioty": ["bazy danych", "programowanie"]
  },
  {
    "id": 13,
    "imie": "Piotr",
    "nazwisko": "Nowak",
    "klasa": "3A",
    "srednia": 3.9,
    "aktywny": true,
    "przedmioty": ["sieci komputerowe"]
  },
  {
    "id": 14,
    "imie": "Maria",
    "nazwisko": "Zielinska",
    "klasa": "3B",
    "srednia": 5.1,
    "aktywny": false,
    "przedmioty": []
  }
]
}

Budowa JSON-a: nawiasy klamrowe { } to obiekt (zbiór par klucz–wartość), nawiasy kwadratowe [ ] to tablica. Klucze zawsze w cudzysłowie podwójnym. Typy wartości: łańcuch znaków, liczba (z kropką dziesiętną, nigdy z przecinkiem), wartość logiczna true albo false, null, obiekt, tablica. Po ostatnim elemencie nie stawia się przecinka — to najczęstszy błąd składniowy w zadaniach z uzupełniania JSON-a.

Odebranie JSON-a i wypełnienie listy — Android
private void pobierzUczniow() {
  pasekPostepu.setVisibility(View.VISIBLE);

  executor.execute(() -> {                      // WATEK ROBOCZY
      try {
          URL adres = new URL("https://api.szkola.pl/uczniowie");
          HttpURLConnection polaczenie = (HttpURLConnection) adres.openConnection();
          polaczenie.setRequestMethod("GET");
          polaczenie.setRequestProperty("Accept", "application/json");
          polaczenie.setConnectTimeout(10000);

          int kod = polaczenie.getResponseCode();
          if (kod != 200) {
              pokazBladNaUI("Serwer zwrocil kod " + kod);
              return;
          }

          String tresc = czytajStrumien(polaczenie.getInputStream());
          polaczenie.disconnect();

          // parsowanie JSON-a na liste obiektow
          List<Uczen> lista = new ArrayList<>();
          JSONObject korzen = new JSONObject(tresc);
          JSONArray tablica = korzen.getJSONArray("uczniowie");

          for (int i = 0; i < tablica.length(); i++) {
              JSONObject o = tablica.getJSONObject(i);
              Uczen u = new Uczen();
              u.id       = o.getInt("id");
              u.imie     = o.getString("imie");
              u.nazwisko = o.getString("nazwisko");
              u.klasa    = o.getString("klasa");
              u.srednia  = o.getDouble("srednia");
              u.aktywny  = o.optBoolean("aktywny", true);  // opt = wartosc domyslna
              lista.add(u);
          }

          // POWROT NA WATEK INTERFEJSU - tylko stad wolno ruszac kontrolki
          runOnUiThread(() -> {
              adapter.ustawDane(lista);
              adapter.notifyDataSetChanged();
              pasekPostepu.setVisibility(View.GONE);
              etykietaLicznik.setText("Znaleziono: " + lista.size());
          });

      } catch (Exception e) {
          pokazBladNaUI("Brak polaczenia z serwerem.");
      }
  });
}
To samo w aplikacji webowej
async function wypelnijListe() {
  const lista = document.getElementById("listaUczniow");
  lista.innerHTML = "<li>Wczytywanie...</li>";

  try {
      const odpowiedz = await fetch("https://api.szkola.pl/uczniowie", {
          method: "GET",
          headers: { "Accept": "application/json" }
      });

      if (!odpowiedz.ok) {                    // ok = kod z zakresu 200-299
          throw new Error("Kod odpowiedzi: " + odpowiedz.status);
      }

      const dane = await odpowiedz.json();    // JSON -> obiekt JavaScript
      lista.innerHTML = "";

      dane.uczniowie.forEach(function (u) {
          const pozycja = document.createElement("li");
          pozycja.textContent = u.nazwisko + " " + u.imie +
                                " (" + u.klasa + ") srednia: " + u.srednia;
          lista.appendChild(pozycja);
      });
  } catch (blad) {
      lista.innerHTML = "<li>Nie udalo sie pobrac danych.</li>";
      console.error(blad);
  }
}

Zwróć uwagę na trzy rzeczy, które sprawdza egzamin: obsługę kodu innego niż 200, obsługę wyjątku przy braku sieci oraz komunikat dla użytkownika na czas oczekiwania. Kod zakładający, że serwer zawsze odpowie poprawnie, traci punkty.

8

Testowanie, wersjonowanie i publikacja

Ostatni etap to wypuszczenie aplikacji do użytkowników. Egzamin pyta tu o rodzaje testów, o różnicę między emulatorem a urządzeniem fizycznym oraz o to, co oznaczają liczby w numerze wersji.

Emulator / symulator

Wirtualne urządzenie uruchomione na komputerze programisty. Zalety: nic nie kosztuje, pozwala błyskawicznie przełączać rozdzielczości, wersje systemu i języki, ma podgląd dzienników i debugger. Wady: nie odda realnej wydajności, zużycia baterii, jakości aparatu, czujników, słabego zasięgu ani zachowania nakładki producenta.

Urządzenie fizyczne

Test na prawdziwym telefonie wymaga włączenia opcji programisty i debugowania po USB. Tylko tu sprawdzisz gesty, czas uruchamiania, zużycie baterii, pracę przy słabym zasięgu i wygląd przy realnej gęstości pikseli. Przed publikacją testuje się na kilku urządzeniach o różnych rozmiarach ekranu i wersjach systemu.

Rodzaje testów aplikacji
  • Testy jednostkowe — sprawdzają pojedynczą metodę lub klasę bez uruchamiania interfejsu (JUnit, NUnit). To kolejny powód, dla którego logika ma siedzieć poza oknem.
  • Testy integracyjne — sprawdzają współpracę modułów, na przykład warstwy logiki z bazą albo z API.
  • Testy interfejsu — automat klika w kontrolki i weryfikuje wyświetlane teksty (w Androidzie Espresso).
  • Testy funkcjonalne i akceptacyjne — czy program robi to, co zapisano w wymaganiach; te ostatnie odbiera klient.
  • Testy wydajnościowe i obciążeniowe — czas reakcji, zużycie pamięci i baterii, zachowanie przy dużej liczbie rekordów.
  • Testy alfa i beta — alfa wewnątrz zespołu, beta na ograniczonej grupie użytkowników zewnętrznych; sklepy udostępniają na to osobne kanały testowe.
  • Testy regresji — powtórzenie wcześniejszych testów po zmianach, żeby sprawdzić, czy poprawka nie zepsuła działającej funkcji.
Wersjonowanie i przygotowanie wydania
// fragment konfiguracji projektu Android
android {
  defaultConfig {
      applicationId "pl.szkola.dziennik"   // unikalny identyfikator w sklepie
      minSdk        24                     // najstarszy obslugiwany system
      targetSdk     34                     // wersja, pod ktora aplikacje testowano
      versionCode   17                     // liczba calkowita, rosnie z KAZDYM wydaniem
      versionName   "2.3.1"                // wersja pokazywana uzytkownikowi
  }
}

Wersjonowanie semantyczne (SemVer):   GLOWNA . POMNIEJSZA . POPRAWKA
                                      2    .     3      .    1

GLOWNA     - zmiany niezgodne wstecz, przebudowa aplikacji
POMNIEJSZA - nowa funkcja, zgodna ze starszymi wersjami
POPRAWKA   - wylacznie naprawa bledow

Rozróżnienie pytane wprost: versionCode to liczba, po której sklep poznaje, że wydanie jest nowsze — musi rosnąć, inaczej plik zostanie odrzucony. versionName to napis dla użytkownika i może wyglądać dowolnie. minSdk ustawiony zbyt wysoko odcina część użytkowników, a ustawiony zbyt nisko zmusza do rezygnacji z nowszych funkcji systemu.

Publikacja aplikacji
  • Kompilacja wydania — wersja release, bez kodu diagnostycznego, z włączonym zmniejszaniem i zaciemnianiem kodu.
  • Podpis cyfrowy — plik aplikacji podpisuje się kluczem. Utrata klucza uniemożliwia wydanie aktualizacji tej samej aplikacji, więc kopia zapasowa klucza jest obowiązkowa.
  • Format pakietu — Android: APK do bezpośredniej instalacji, AAB (Android App Bundle) do publikacji w Google Play; iOS: IPA wysyłane do App Store; komputer: instalator EXE, MSI, DEB, RPM albo obraz DMG.
  • Karta w sklepie — nazwa, opis, zrzuty ekranu, ikona, kategoria wiekowa i informacja o przetwarzanych danych. Aplikacja przechodzi weryfikację przed publikacją.
  • Wydanie etapowe — udostępnienie najpierw niewielkiemu odsetkowi użytkowników, żeby wychwycić awarie, zanim dotkną wszystkich.
  • Po publikacji — monitorowanie raportów o awariach i ocen użytkowników, poprawki w kolejnych wersjach. Aktualizacja aplikacji desktopowej wymaga własnego mechanizmu aktualizacji albo ponownej instalacji.
Najczęstsze pułapki egzaminacyjne
  • Mylenie pola wyboru z przyciskiem opcji. CheckBox — wiele zaznaczeń niezależnych. RadioButton — dokładnie jedno w obrębie grupy.
  • Pominięcie onDestroy przy obrocie ekranu. Obrót to pełne odtworzenie aktywności: onPause, onStop, onDestroy, a potem onCreate, onStart, onResume. Odpowiedź „wywoła się tylko onCreate" jest błędna.
  • Mylenie onSaveInstanceState z zapisem trwałym. Bundle przeżywa obrót ekranu, ale nie zamknięcie aplikacji. Dane, które mają zostać na stałe, idą do preferencji, pliku albo bazy.
  • Zapytanie sieciowe w wątku interfejsu. Blokuje pętlę zdarzeń, więc aplikacja „zamarza", a w Androidzie kończy się komunikatem ANR. Sieć zawsze w tle, aktualizacja kontrolek zawsze z powrotem w wątku interfejsu.
  • Uznawanie 404 za błąd serwera. 4xx to wina klienta (zły adres, brak pola, brak tokenu), 5xx to wina serwera. 401 znaczy „nie wiem, kim jesteś", a 403 — „wiem, ale nie wolno ci".
  • Przecinek po ostatnim elemencie JSON-a albo klucz bez cudzysłowów. Parser zgłasza wtedy błąd składni i całe zadanie idzie na zero.
  • Uznawanie wpisu w manifeście za komplet. Dla uprawnień niebezpiecznych trzeba dodatkowo zapytać użytkownika w trakcie działania aplikacji i obsłużyć odmowę.
  • Mylenie versionCode z versionName. Sklep porównuje wydania po liczbie versionCode; napis w versionName nie ma dla niego znaczenia.
Sprawdź, ile z tego zostało w głowie

Kolejność metod cyklu życia i kody odpowiedzi HTTP wchodzą do głowy dopiero po kilku powtórzeniach. Rozwiąż test z tego działu i zobacz, które scenariusze jeszcze mylisz.