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.
📋 Spis treści
Typy aplikacji i interfejs
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.
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.
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.
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.
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.
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.
| Typ | Dostęp do sprzętu | Wydajność | Koszt wytworzenia | Dystrybucja | Technologie |
|---|---|---|---|---|---|
| Desktopowa | pełny (pliki, porty, drukarki) | bardzo wysoka | średni, rośnie z liczbą systemów | instalator EXE, MSI, DEB | WPF, JavaFX, Qt |
| Webowa | ograniczony, przez API przeglądarki | średnia, zależna od łącza | niski — jeden kod na wszystko | adres URL, bez instalacji | HTML, CSS, JS, PHP |
| Mobilna natywna | pełny (GPS, aparat, NFC, czujniki) | najwyższa na danym systemie | wysoki — osobny kod na system | Google Play, App Store | Kotlin, Java, Swift |
| Hybrydowa | przez wtyczki pomostowe | niższa niż natywna | niski — jeden kod na dwa systemy | sklepy z aplikacjami | Cordova, Ionic |
| Wieloplatformowa | szeroki, przez gotowe moduły | zbliżona do natywnej | średni | sklepy z aplikacjami | Flutter, React Native, MAUI |
| PWA | ograniczony, jak w przeglądarce | średnia | najniższy | instalacja z przeglądarki | JS + 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.
| Nazwa polska | Nazwa angielska | Do czego służy | Typowe zdarzenie |
|---|---|---|---|
| Etykieta | Label / TextView | wyświetla tekst opisujący inną kontrolkę lub wynik; użytkownik jej nie edytuje | zwykle brak, opcjonalnie kliknięcie |
| Pole tekstowe | TextBox / EditText | wprowadzanie tekstu lub liczby przez użytkownika | zmiana tekstu, utrata fokusu |
| Przycisk | Button | uruchamia akcję: zapis, obliczenie, wysłanie formularza | kliknięcie, długie naciśnięcie |
| Lista rozwijana | ComboBox / Spinner | wybór jednej pozycji z wielu bez zajmowania miejsca na ekranie | zmiana zaznaczonej pozycji |
| Pole wyboru | CheckBox | niezależne włącz/wyłącz; można zaznaczyć wiele naraz | zmiana stanu zaznaczenia |
| Przycisk opcji | RadioButton | wybór dokładnie jednej opcji w grupie — zaznaczenie jednej odznacza pozostałe | zmiana zaznaczenia w grupie |
| Lista pozycji | ListBox / RecyclerView | prezentacja zbioru rekordów, zwykle z przewijaniem | kliknięcie pozycji listy |
| Tabela | DataGridView / TableLayout | dane w wierszach i kolumnach, zwykle z bazy; sortowanie i edycja komórek | zmiana wiersza, edycja komórki |
| Menu | MenuStrip / Menu | grupuje polecenia programu (Plik, Edycja, Pomoc) i skróty klawiszowe | wybó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.
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.
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.
- 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.
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 procesZapamię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).
// 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.
- 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.
- 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.
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.
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.
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.
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.
- 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
}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.
- 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.
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 | Kiedy jest wywoływana | Co w niej robić |
|---|---|---|
| onCreate() | raz, przy tworzeniu aktywności | ustawić układ (setContentView), odnaleźć kontrolki, podpiąć zdarzenia, odtworzyć stan z parametru savedInstanceState |
| onStart() | gdy aktywność staje się widoczna | zarejestrować odbiorniki zdarzeń systemowych, rozpocząć obserwację danych |
| onResume() | gdy aktywność trafia na wierzch i przyjmuje dotyk | wznowić 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ć widoczna | zapisać dane trwale (baza, preferencje), zwolnić cięższe zasoby, wyrejestrować odbiorniki |
| onRestart() | przed onStart, gdy wracamy do zatrzymanej aktywności | odś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 |
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ć.
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.
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.
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ń.
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.
-- 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.
- 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.
- 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.
| Kod | Nazwa | Znaczenie | Reakcja aplikacji |
|---|---|---|---|
| 200 | OK | żądanie wykonane, dane w treści odpowiedzi | przetwarza JSON i wyświetla wynik |
| 201 | Created | zasób został utworzony — typowa odpowiedź na POST | czyści formularz, odświeża listę |
| 400 | Bad Request | błędne żądanie: brak pola, zły format danych | pokazuje błąd walidacji przy polu formularza |
| 401 | Unauthorized | brak uwierzytelnienia albo token wygasł | przenosi na ekran logowania |
| 403 | Forbidden | użytkownik jest rozpoznany, ale nie ma uprawnień | informuje o braku dostępu |
| 404 | Not Found | nie ma zasobu pod tym adresem (zły identyfikator, literówka w URL) | komunikat „nie znaleziono" |
| 500 | Internal Server Error | błą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.
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.
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.");
}
});
}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.
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.
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.
- 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.
// 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 bledowRozróż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.
- 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.
- 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.
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.