school-it.pl // android + kotlin // rozdział 5 // lekcja 19
Pola klasy i initUI() — porządek w kodzie
Od pierwszej lekcji cały kod był w onCreate. Przy małych aplikacjach to wygodne, ale w lekcji 18 onCreate miało już ponad 80 linii: wyszukiwanie kontrolek, zmienne, funkcje w środku funkcji, listenery i logikę naraz. W tej lekcji uporządkujesz taki kod: kontrolki staną się polami klasy, wyszukasz je w jednej metodzie initUI(), a logikę rozdzielisz na krótkie metody z nazwami mówiącymi, co robią. Od tej lekcji tak wygląda kod we wszystkich przykładach kursu.
Czego się dziś nauczysz
- Odróżnisz zmienną lokalną od pola klasy i powiesz, gdzie każda jest widoczna.
- Zadeklarujesz kontrolki jako pola
private lateinit var. - Zbierzesz wszystkie
findViewByIdw metodzieinitUI(). - Rozdzielisz
onCreatena krótkie metody o czytelnych nazwach. - Przerobisz działającą aplikację bez zmiany jej działania (refaktoryzacja).
Problem: wszystko w onCreate
Przypomnij sobie licznik sali z lekcji 05. Zmienne zadeklarowane w onCreate są lokalne — istnieją tylko wewnątrz tej metody. Dlatego funkcja pokazStan() musiała być napisana w środku onCreate: gdyby stała obok, nie widziałaby ani kontrolek, ani zmiennej osoby.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvLicznik = findViewById<TextView>(R.id.tvLicznik) // lokalne — widać je
val btnPlus = findViewById<Button>(R.id.btnPlus) // tylko w onCreate
// … jeszcze trzy findViewById
var osoby = 0
fun pokazStan() { … } // funkcja w funkcji
btnPlus.setOnClickListener { … } // listenery
// … i tak dalej, 60 linii
}
Taki kod działa, ale ma trzy wady: onCreate robi wszystko naraz, trudno znaleźć konkretne miejsce, a żadnej części nie da się wydzielić do osobnej metody klasy.
Pola klasy
Pole to zmienna zadeklarowana w klasie, poza metodami. Widzą ją wszystkie metody tej klasy i istnieje tak długo, jak obiekt aktywności:
widoczne w każdej metodzie
żyje razem z ekranem
widoczna tylko w niej
znika po jej zakończeniu
| Co | Gdzie deklarować |
|---|---|
| kontrolki ekranu | pola: private lateinit var |
| stan aplikacji (licznik, wybrany numer, wpisany PIN) | pola: private var |
| stałe (limit, tablica danych) | pola: private val |
| wyniki pośrednie obliczeń | zmienne lokalne w metodzie, która ich używa |
Słowo private oznacza, że pole jest prywatne dla tej klasy — żadna inna klasa go nie zmieni. Kontrolki i stan ekranu zawsze oznaczamy jako prywatne.
lateinit var
Pole na kontrolkę ma kłopot: w chwili tworzenia obiektu aktywności ekranu jeszcze nie ma — setContentView wykona się dopiero w onCreate. Nie można więc od razu przypisać kontrolki. Kotlin wymaga jednak, żeby każde pole miało wartość. Rozwiązaniem jest lateinit — „wartość przypiszę później, obiecuję, że przed pierwszym użyciem”:
class MainActivity : AppCompatActivity() {
private lateinit var tvLicznik: TextView // typ podany, wartości jeszcze brak
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvLicznik = findViewById(R.id.tvLicznik) // teraz pole dostaje wartość
}
}
| Zasada | Wyjaśnienie |
|---|---|
zawsze var | Wartość przypisujesz później, więc musi się dać ją zmienić. |
| trzeba podać typ | : TextView — Kotlin nie ma z czego go odgadnąć. |
nie dla Int, Boolean, Double | Liczby i wartości logiczne mają zwykłą wartość startową: private var osoby = 0. |
findViewById(…) bez <TextView> | Typ wynika z pola, więc nawiasy ostre można pominąć. |
Gdy złamiesz obietnicę i użyjesz pola przed przypisaniem, aplikacja zamknie się z czytelnym komunikatem: lateinit property tvLicznik has not been initialized.
Metoda initUI()
Wszystkie przypisania kontrolek zbieramy w jednej prywatnej metodzie initUI() (od initialize user interface — „przygotuj interfejs”). Wywołujemy ją w onCreate zaraz po setContentView:
private fun initUI() {
tvLicznik = findViewById(R.id.tvLicznik)
tvInfo = findViewById(R.id.tvInfo)
btnPlus = findViewById(R.id.btnPlus)
btnMinus = findViewById(R.id.btnMinus)
}
Gdy na ekranie pojawi się nowa kontrolka, dopisujesz dwie linie w dwóch przewidywalnych miejscach: pole na górze klasy i przypisanie w initUI().
Krótkie metody
Gdy kontrolki i stan są polami, każda część kodu może być osobną metodą klasy. onCreate zamienia się w spis treści:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI() // 1. znajdź kontrolki
ustawZdarzenia() // 2. podepnij listenery
pokazStan() // 3. pokaż stan początkowy
}
| Metoda | Co zawiera |
|---|---|
initUI() | tylko findViewById |
ustawZdarzenia() | tylko setOn…Listener, a w nich wywołania metod — bez długiej logiki |
dodaj(), usun(), sprawdz()… | to, co się dzieje po kliknięciu — jedna czynność na metodę |
pokazStan() | przeniesienie stanu na ekran: teksty, kolory, isEnabled |
Nazwa metody to czasownik, który mówi, co ona robi. Listener zawiera najczęściej jedną linię: btnPlus.setOnClickListener { dodajOsobe() } — czytasz go jak zdanie.
Czy tak się pisze w 2026?
Uczciwa odpowiedź
Pola lateinit var z findViewById w initUI() to poprawny i czytelny sposób, który spotkasz w bardzo wielu projektach, materiałach i zadaniach egzaminacyjnych. Nie jest to jednak najnowsze zalecenie Google. Dla ekranów w XML Google zaleca dziś View Binding — Android Studio samo generuje klasę z polami dla wszystkich kontrolek, więc nie piszesz ani jednego findViewById. Poznasz go w lekcji 38. Nowe aplikacje coraz częściej powstają w Jetpack Compose, bez plików XML.
To, czego uczysz się teraz — kontrolki jako pola, krótki onCreate, małe metody z dobrymi nazwami — zostaje także przy View Binding. Zmieni się tylko sposób znalezienia kontrolek.
Licznik sali — wersja uporządkowana
Ten sam licznik osób co w lekcji 05: limit 10 osób, przyciski + i − wyłączają się na granicach, licznik czerwienieje przy komplecie, zerowanie przytrzymaniem. Układ XML jest bez zmian — skopiuj go z lekcji 05. Zmienia się tylko MainActivity.kt. Działanie aplikacji ma być identyczne — tak wygląda refaktoryzacja: lepszy kod, ten sam efekt.
aplikacja,
inny kod
Przygotowanie projektu
- Otwórz projekt
LicznikSaliz lekcji 05 (albo utwórz nowy i skopiujactivity_main.xmlz lekcji 05). - Podmień
MainActivity.ktna kod poniżej. Uruchom i sprawdź, że wszystko działa jak wcześniej.
package pl.schoolit.liczniksali
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var tvLicznik: TextView
private lateinit var tvInfo: TextView
private lateinit var btnPlus: Button
private lateinit var btnMinus: Button
private lateinit var btnZeruj: Button
// ---------- stan i stałe ----------
private val limit = 10
private var osoby = 0
// ---------- start ekranu ----------
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
ustawZdarzenia()
pokazStan()
}
private fun initUI() {
tvLicznik = findViewById(R.id.tvLicznik)
tvInfo = findViewById(R.id.tvInfo)
btnPlus = findViewById(R.id.btnPlus)
btnMinus = findViewById(R.id.btnMinus)
btnZeruj = findViewById(R.id.btnZeruj)
}
private fun ustawZdarzenia() {
btnPlus.setOnClickListener { dodajOsobe() }
btnMinus.setOnClickListener { usunOsobe() }
btnZeruj.setOnClickListener { tvInfo.text = "Przytrzymaj przycisk, aby wyzerować" }
btnZeruj.setOnLongClickListener {
zeruj()
true
}
}
// ---------- czynności ----------
private fun dodajOsobe() {
osoby++
pokazStan()
}
private fun usunOsobe() {
osoby--
pokazStan()
}
private fun zeruj() {
osoby = 0
pokazStan()
}
// ---------- stan → ekran ----------
private fun pokazStan() {
tvLicznik.text = "$osoby / $limit"
btnMinus.isEnabled = osoby > 0
btnPlus.isEnabled = osoby < limit
if (osoby == limit) {
tvLicznik.setTextColor(Color.parseColor("#C62828"))
tvInfo.text = "Sala pełna!"
} else {
tvLicznik.setTextColor(Color.BLACK)
tvInfo.text = "Wolnych miejsc: ${limit - osoby}"
}
}
}
Co się zmieniło
| Lekcja 05 | Lekcja 19 |
|---|---|
val tvLicznik = findViewById<TextView>(…) w onCreate | pole private lateinit var tvLicznik: TextView + przypisanie w initUI() |
var osoby = 0 w onCreate | pole private var osoby = 0 |
fun pokazStan() wewnątrz onCreate | zwykła metoda klasy private fun pokazStan() |
| logika w klamrach listenerów | listenery wywołują metody o nazwach-czasownikach |
onCreate — ok. 55 linii | onCreate — 6 linii, czytany jak spis treści |
Komentarze-nagłówki // ---------- … ---------- dzielą klasę na części. Nie są obowiązkowe, ale przy większych ekranach pomagają szybko znaleźć miejsce. Kolejność zawsze ta sama: pola → onCreate → initUI → ustawZdarzenia → czynności → wyświetlanie.
Sprawdź trzy rzeczy
- Uruchom aplikację i przejdź wszystkie scenariusze z lekcji 05. Działanie musi być identyczne.
- Zamień w
onCreatekolejność:initUI()przedsetContentView. Aplikacja zamknie się — w Logcat znajdziesz NullPointerException, bofindViewByIdnie ma jeszcze w czym szukać. - Usuń wywołanie
initUI(). Teraz w Logcat będzie lateinit property tvLicznik has not been initialized — komunikat mówi dokładnie, którego pola brakuje.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
private lateinit var nazwa: Typ | pole | Kontrolka przypisywana później (w initUI). |
private var nazwa = wartość | pole | Stan ekranu z wartością startową. |
private val nazwa = wartość | pole | Stała dostępna w całej klasie. |
private fun initUI() | metoda | Wszystkie findViewById w jednym miejscu. |
private fun ustawZdarzenia() | metoda | Podpięcie listenerów. |
findViewById(R.id.…) bez <Typ> | metoda | Typ wynika z pola, do którego przypisujesz. |
private | modyfikator | Widoczne tylko w tej klasie. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| lateinit property … has not been initialized | Nie wywołano initUI(), albo pole użyte przed nim, albo brak przypisania tego pola w initUI(). |
private val tv = findViewById<TextView>(R.id.tv) jako pole | Awaria przy starcie — pole tworzy się przed onCreate, gdy ekranu jeszcze nie ma. |
private lateinit val tv: TextView | Błąd kompilacji: ’lateinit’ modifier is allowed only on mutable properties. Musi być var. |
private lateinit var osoby: Int | Błąd kompilacji: ’lateinit’ modifier is not allowed on properties of primitive types. Napisz private var osoby = 0. |
val tvLicznik = findViewById<TextView>(…) w initUI() | Tworzy nową zmienną lokalną o tej samej nazwie zamiast przypisać pole — pole zostaje puste (Android Studio pokaże tylko ostrzeżenie Name shadowed). Bez val: tvLicznik = findViewById(…). |
| Not enough information to infer type variable T | findViewById bez typu przypisany do zmiennej bez typu. Przy polach typ jest podany — przy zmiennych lokalnych dopisz <TextView>. |
Zadania
W zadaniach 1–4 przerabiasz własne projekty z poprzednich lekcji. Po przeróbce aplikacja musi działać dokładnie tak samo jak przed nią. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Przerób przykład z lekcji 06: pola na kontrolki, initUI(), ustawZdarzenia(), metody oblicz() i wyczysc().
pokaż wskazówkę
- W metodzie
oblicz()zamiastreturn@setOnClickListenerwystarczy zwykłereturn— to już osobna metoda, a nie lambda.
Przerób przykład z lekcji 09. Funkcja przelicz() ma być metodą klasy, a sześć pól dodatków — trzymaj w tablicy będącej polem klasy, żeby listenery podpiąć pętlą.
pokaż wskazówkę
- Pole:
private lateinit var dodatki: Array<CheckBox>, a winitUI():dodatki = arrayOf(findViewById(R.id.cbSer), …).
Przerób przykład z lekcji 18. Wydziel metody: wpiszCyfre(cyfra: String), usunCyfre(), sprawdzPin(), zablokuj(), pokazStan(), ustawKlawiature(wlaczona: Boolean). onCreate nie może mieć więcej niż 7 linii.
pokaż wskazówkę
- Tablice
cyfryikropkiteż są polami (lateinit var … : Array<Button>). Listener cyfry:k.setOnClickListener { wpiszCyfre(k.text.toString()) }.
Przerób przykład z lekcji 13 (rezerwacja sali). Następnie celowo wprowadź trzy błędy z tabeli „Zanim utkniesz” (po jednym naraz), zapisz dokładny komunikat każdego z nich i miejsce, w którym go znalazłeś (okno Build czy Logcat).
pokaż wskazówkę
- Najbardziej podstępny jest
val … = findViewById<TextView>(…)winitUI()— kompiluje się, a aplikacja zamyka się dopiero przy pierwszym użyciu pola.
Nowa aplikacja od razu w uporządkowanej formie: plansza 3 × 3 z przycisków, napis „Ruch: X”, przycisk Nowa gra. Metody: wykonajRuch(pole: Int), sprawdzWygrana(): Boolean, nowaGra(), pokazStan(). Po wygranej plansza się blokuje i pokazuje „Wygrywa O!”.
pokaż wskazówkę
- Stan planszy:
private val plansza = Array(9) { "" }. Wygrane linie zapisz jako tablicę trójek indeksów:arrayOf(intArrayOf(0, 1, 2), …)i sprawdzaj pętlą.
Wymień się z kolegą lub koleżanką kodem z zadania 5. Napisz w komentarzach na początku jego pliku MainActivity.kt co najmniej pięć uwag: która metoda robi za dużo, które nazwy są niejasne, które zmienne powinny być lokalne, a które polami, gdzie powtarza się kod. Potem popraw własny kod według uwag, które dostałeś.
pokaż wskazówkę
- Sygnał ostrzegawczy: metoda dłuższa niż ekran, nazwa typu
zrob()lubmetoda2(), ten sam blok kodu w dwóch miejscach, pole używane tylko w jednej metodzie.
Co trzeba zapamiętać
- Zmienna lokalna żyje w jednej metodzie; pole klasy — w całej klasie, tak długo jak ekran.
- Kontrolki:
private lateinit var nazwa: Typ, przypisywane winitUI()posetContentView. - Stan:
private varz wartością startową; stałe:private val. onCreateto spis treści:initUI(),ustawZdarzenia(),pokazStan().- Listener wywołuje metodę-czasownik; jedna metoda = jedna czynność.
- Ten wzorzec to krok w stronę View Binding (lekcja 38), który zaleca dziś Google.
Dokumentacja: lateinit w Kotlinie, Modyfikatory widoczności.