Przejdź do treści
School IT / Przestrzeń do nauki

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.

Kotlin lateinit var private initUI() metody refaktoryzacja 1 × 45 min
CEL LEKCJI

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 findViewById w metodzie initUI().
  • Rozdzielisz onCreate na krótkie metody o czytelnych nazwach.
  • Przerobisz działającą aplikację bez zmiany jej działania (refaktoryzacja).
TEORIA

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.

tak było — skrót kodu z lekcji 05
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.

TEORIA

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:

pole klasy
deklaracja w klasie
widoczne w każdej metodzie
żyje razem z ekranem
zmienna lokalna
deklaracja w metodzie
widoczna tylko w niej
znika po jej zakończeniu
CoGdzie deklarować
kontrolki ekranupola: 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.

TEORIA

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”:

deklaracja i późniejsze przypisanie
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ść
    }
}
ZasadaWyjaśnienie
zawsze varWartość 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, DoubleLiczby 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.

TEORIA

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:

initUI()
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().

TEORIA

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:

onCreate jako 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
}
MetodaCo 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.

TEORIA

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.

PRZYKŁAD

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.

18:12
Koncert — sala 12
7 / 10
Wolnych miejsc: 3
−
+
Zeruj (przytrzymaj)
wygląd i działanie jak w lekcji 05
=ta sama
aplikacja,
inny kod
pola
5 kontrolek + stan
onCreate
initUI() · ustawZdarzenia() · pokazStan()
metody czynności
dodajOsobe() · usunOsobe() · zeruj()
pokazStan()
stan → ekran
budowa klasy MainActivity

Przygotowanie projektu

  1. Otwórz projekt LicznikSali z lekcji 05 (albo utwórz nowy i skopiuj activity_main.xml z lekcji 05).
  2. Podmień MainActivity.kt na kod poniżej. Uruchom i sprawdź, że wszystko działa jak wcześniej.
MainActivity.kt — pełny plik
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 05Lekcja 19
val tvLicznik = findViewById<TextView>(…) w onCreatepole private lateinit var tvLicznik: TextView + przypisanie w initUI()
var osoby = 0 w onCreatepole private var osoby = 0
fun pokazStan() wewnątrz onCreatezwykła metoda klasy private fun pokazStan()
logika w klamrach listenerówlistenery wywołują metody o nazwach-czasownikach
onCreate — ok. 55 liniionCreate — 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

  1. Uruchom aplikację i przejdź wszystkie scenariusze z lekcji 05. Działanie musi być identyczne.
  2. Zamień w onCreate kolejność: initUI() przed setContentView. Aplikacja zamknie się — w Logcat znajdziesz NullPointerException, bo findViewById nie ma jeszcze w czym szukać.
  3. 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.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
private lateinit var nazwa: TyppoleKontrolka przypisywana później (w initUI).
private var nazwa = wartośćpoleStan ekranu z wartością startową.
private val nazwa = wartośćpoleStała dostępna w całej klasie.
private fun initUI()metodaWszystkie findViewById w jednym miejscu.
private fun ustawZdarzenia()metodaPodpięcie listenerów.
findViewById(R.id.…) bez <Typ>metodaTyp wynika z pola, do którego przypisujesz.
privatemodyfikatorWidoczne tylko w tej klasie.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
lateinit property … has not been initializedNie 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 poleAwaria przy starcie — pole tworzy się przed onCreate, gdy ekranu jeszcze nie ma.
private lateinit val tv: TextViewBłąd kompilacji: ’lateinit’ modifier is allowed only on mutable properties. Musi być var.
private lateinit var osoby: IntBłą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 TfindViewById bez typu przypisany do zmiennej bez typu. Przy polach typ jest podany — przy zmiennych lokalnych dopisz <TextView>.
ZADANIA

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.

ZAD 1Podział rachunku★☆☆

Przerób przykład z lekcji 06: pola na kontrolki, initUI(), ustawZdarzenia(), metody oblicz() i wyczysc().

pokaż wskazówkę
  • W metodzie oblicz() zamiast return@setOnClickListener wystarczy zwykłe return — to już osobna metoda, a nie lambda.
ZAD 2Zamówienie pizzy★☆☆

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 w initUI(): dodatki = arrayOf(findViewById(R.id.cbSer), …).
ZAD 3Klawiatura PIN★★☆

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 cyfry i kropki też są polami (lateinit var … : Array<Button>). Listener cyfry: k.setOnClickListener { wpiszCyfre(k.text.toString()) }.
ZAD 4Znajdź i popraw★★☆

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>(…) w initUI() — kompiluje się, a aplikacja zamyka się dopiero przy pierwszym użyciu pola.
ZAD 5Kółko i krzyżyk★★☆

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ą.
ZAD 6Recenzja kodu kolegi★★★

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() lub metoda2(), ten sam blok kodu w dwóch miejscach, pole używane tylko w jednej metodzie.
PODSUMOWANIE

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 w initUI() po setContentView.
  • Stan: private var z wartością startową; stałe: private val.
  • onCreate to 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.

Postęp zapisuje się w tej przeglądarce.