school-it.pl // android + kotlin // rozdział 3 // lekcja 09
CheckBox — wiele opcji
CheckBox to pole wyboru: kwadracik, który można zaznaczyć albo odznaczyć. Każdy działa niezależnie, więc nadaje się wszędzie tam, gdzie użytkownik wybiera dowolnie wiele opcji naraz — dodatki do pizzy, zainteresowania, zgody w formularzu. W tej lekcji zbudujesz zamówienie pizzy, w którym cena przelicza się na bieżąco przy każdym kliknięciu.
Czego się dziś nauczysz
- Dodasz pola wyboru i ustawisz ich stan początkowy.
- Odczytasz, czy pole jest zaznaczone (
isChecked). - Zaznaczysz i odznaczysz pole z kodu.
- Zareagujesz na każdą zmianę zaznaczenia.
- Policzysz sumę i zbudujesz listę wybranych opcji.
CheckBox w XML
<CheckBox
android:id="@+id/cbSer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Podwójny ser (+4 zł)" />
<CheckBox
android:id="@+id/cbRegulamin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="Akceptuję regulamin" />
android:text to podpis obok kwadracika. Kliknięcie w podpis też przełącza pole — cały wiersz jest celem dotyku. android:checked="true" zaznacza pole na starcie.
Odczyt i zmiana stanu
Stan pola to właściwość isChecked typu Boolean. Można ją czytać i zmieniać:
val cbRegulamin = findViewById<CheckBox>(R.id.cbRegulamin)
btnWyslij.setOnClickListener {
if (cbRegulamin.isChecked) { // odczyt
tvInfo.text = "Wysłano"
} else {
tvInfo.text = "Zaakceptuj regulamin"
}
}
btnWyczysc.setOnClickListener {
cbRegulamin.isChecked = false // zmiana z kodu
}
Pole można też przełączyć metodą toggle() — zaznaczone staje się odznaczone i odwrotnie.
Reakcja na zmianę
Czasem stan pola odczytujesz dopiero po kliknięciu przycisku. Częściej jednak ekran ma reagować od razu — na przykład cena ma się zmienić w chwili zaznaczenia dodatku. Do tego służy setOnCheckedChangeListener:
cbRegulamin.setOnCheckedChangeListener { _, zaznaczony ->
btnWyslij.isEnabled = zaznaczony // przycisk aktywny tylko po akceptacji
}
Android przekazuje do klamer dwie wartości: pole, które się zmieniło (tu niepotrzebne — _), i jego nowy stan (zaznaczony). Kod wykonuje się przy każdej zmianie — także wtedy, gdy zmienisz isChecked z kodu.
setOnClickListener a setOnCheckedChangeListener
Na polu wyboru zadziała też setOnClickListener, ale tylko dla kliknięć palcem. setOnCheckedChangeListener wywoła się przy każdej zmianie stanu — także z kodu, np. po przycisku „Wyczyść”. Dlatego do pól wyboru używamy tego drugiego.
Wiele pól naraz
Przy kilku polach zwykle trzeba policzyć coś na podstawie wszystkich — sumę, liczbę zaznaczonych, listę nazw. Wygodnie jest napisać jedną funkcję, która to robi, i wywołać ją z listenera każdego pola. Każde pole ma przy tym ten sam kod w klamrach:
fun przelicz() {
var suma = 0
if (cbSer.isChecked) suma += 4
if (cbSzynka.isChecked) suma += 5
tvSuma.text = "Dodatki: $suma zł"
}
cbSer.setOnCheckedChangeListener { _, _ -> przelicz() }
cbSzynka.setOnCheckedChangeListener { _, _ -> przelicz() }
Gdy if ma tylko jedną instrukcję, można zapisać go w jednej linii bez klamer: if (warunek) suma += 4.
Zamówienie pizzy
Pizza bazowa kosztuje 24 zł. Sześć dodatków ma różne ceny. Cena końcowa i lista dodatków zmieniają się w chwili zaznaczenia lub odznaczenia pola. Przy więcej niż czterech dodatkach pojawia się rabat 3 zł. Przycisk Bez dodatków odznacza wszystko, a Zamów jest aktywny tylko po zaznaczeniu zgody na regulamin.
3 dodatków
i regulaminu
24 zł + 4 + 3 + 2 = 33 zł; przy pięciu i więcej dodatkach cena spada o 3 zł
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Pizza, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Formularz jest wScrollView— na małym ekranie dół trzeba przewinąć.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pizza Margherita · 24 zł"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Dodatki"
android:textStyle="bold" />
<CheckBox android:id="@+id/cbSer"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Podwójny ser (+4 zł)" />
<CheckBox android:id="@+id/cbSzynka"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Szynka (+5 zł)" />
<CheckBox android:id="@+id/cbPieczarki"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Pieczarki (+3 zł)" />
<CheckBox android:id="@+id/cbOliwki"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Oliwki (+3 zł)" />
<CheckBox android:id="@+id/cbJalapeno"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Jalapeño (+2 zł)" />
<CheckBox android:id="@+id/cbAnanas"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Ananas (+4 zł)" />
<Button
android:id="@+id/btnBezDodatkow"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bez dodatków" />
<TextView
android:id="@+id/tvDodatki"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp" />
<TextView
android:id="@+id/tvCena"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/cbRegulamin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Akceptuję regulamin" />
<Button
android:id="@+id/btnZamow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="Zamów" />
<TextView
android:id="@+id/tvPotwierdzenie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="#2E7D32" />
</LinearLayout>
</ScrollView>
package pl.schoolit.pizza
import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val cbSer = findViewById<CheckBox>(R.id.cbSer)
val cbSzynka = findViewById<CheckBox>(R.id.cbSzynka)
val cbPieczarki = findViewById<CheckBox>(R.id.cbPieczarki)
val cbOliwki = findViewById<CheckBox>(R.id.cbOliwki)
val cbJalapeno = findViewById<CheckBox>(R.id.cbJalapeno)
val cbAnanas = findViewById<CheckBox>(R.id.cbAnanas)
val cbRegulamin = findViewById<CheckBox>(R.id.cbRegulamin)
val btnBezDodatkow = findViewById<Button>(R.id.btnBezDodatkow)
val btnZamow = findViewById<Button>(R.id.btnZamow)
val tvDodatki = findViewById<TextView>(R.id.tvDodatki)
val tvCena = findViewById<TextView>(R.id.tvCena)
val tvPotwierdzenie = findViewById<TextView>(R.id.tvPotwierdzenie)
val cenaBazowa = 24
// liczy cenę i listę dodatków na podstawie WSZYSTKICH pól
fun przelicz() {
var cena = cenaBazowa
var ile = 0
var lista = ""
if (cbSer.isChecked) { cena += 4; ile++; lista += "ser, " }
if (cbSzynka.isChecked) { cena += 5; ile++; lista += "szynka, " }
if (cbPieczarki.isChecked) { cena += 3; ile++; lista += "pieczarki, " }
if (cbOliwki.isChecked) { cena += 3; ile++; lista += "oliwki, " }
if (cbJalapeno.isChecked) { cena += 2; ile++; lista += "jalapeño, " }
if (cbAnanas.isChecked) { cena += 4; ile++; lista += "ananas, " }
if (ile >= 5) {
cena -= 3
}
if (ile == 0) {
tvDodatki.text = "Bez dodatków"
} else {
tvDodatki.text = lista.removeSuffix(", ") // bez przecinka na końcu
}
tvCena.text = "Do zapłaty: $cena zł"
tvPotwierdzenie.text = "" // zmiana unieważnia zamówienie
}
przelicz()
// każde pole dodatku po zmianie przelicza całość
cbSer.setOnCheckedChangeListener { _, _ -> przelicz() }
cbSzynka.setOnCheckedChangeListener { _, _ -> przelicz() }
cbPieczarki.setOnCheckedChangeListener { _, _ -> przelicz() }
cbOliwki.setOnCheckedChangeListener { _, _ -> przelicz() }
cbJalapeno.setOnCheckedChangeListener { _, _ -> przelicz() }
cbAnanas.setOnCheckedChangeListener { _, _ -> przelicz() }
// regulamin włącza i wyłącza przycisk
cbRegulamin.setOnCheckedChangeListener { _, zaznaczony ->
btnZamow.isEnabled = zaznaczony
}
btnBezDodatkow.setOnClickListener {
cbSer.isChecked = false
cbSzynka.isChecked = false
cbPieczarki.isChecked = false
cbOliwki.isChecked = false
cbJalapeno.isChecked = false
cbAnanas.isChecked = false
// przelicz() wywoła się samo — zmiana z kodu też uruchamia listener
}
btnZamow.setOnClickListener {
tvPotwierdzenie.text = "Przyjęto zamówienie. ${tvCena.text}"
}
}
}
Co dzieje się po kolei
przelicz()zaczyna od ceny bazowej i dla każdego zaznaczonego pola dolicza cenę, zwiększa licznik i dopisuje nazwę do listy. Średniki pozwalają zmieścić trzy krótkie instrukcje w jednej linii.- Na końcu
removeSuffix(", ")usuwa ostatni przecinek — lista „ser, pieczarki, ” staje się „ser, pieczarki”. - Wszystkie pola dodatków mają ten sam listener:
{ _, _ -> przelicz() }. Nie interesuje nas, które pole się zmieniło — liczymy wszystko od nowa. btnBezDodatkowtylko odznacza pola. Każde odznaczenie uruchamia listener, więc cena przelicza się sama.btnZamowma w XMLenabled="false". Włącza go dopiero zaznaczenie regulaminu — wartośćzaznaczonytrafia wprost doisEnabled.
Sprawdź trzy rzeczy
- Zaznacz pięć dodatków. Cena wyniesie 24 + suma − 3 zł. Odznacz jeden — rabat zniknie.
- Zamów, a potem zmień dodatek. Potwierdzenie zniknie — zamówienie nie odpowiada już wyborowi.
- Kliknij w napis „Szynka”, a nie w kwadracik. Pole też się zaznaczy.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<CheckBox> | kontrolka | Pole wyboru z podpisem. |
android:checked="true" | atrybut | Zaznaczone na starcie. |
cb.isChecked | Boolean | Odczyt i zmiana stanu. |
cb.toggle() | metoda | Przełącza stan na przeciwny. |
cb.setOnCheckedChangeListener { _, zaznaczony -> } | metoda | Kod po każdej zmianie stanu. |
Widget.Material3.Button.TextButton | styl | Przycisk wyglądający jak tekst. |
napis.removeSuffix(", ") | String | Usuwa podany fragment z końca, jeśli tam jest. |
if (warunek) instrukcja | skrót | if bez klamer dla jednej instrukcji. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
if (cbSer.isChecked == "true") | Błąd kompilacji — isChecked to Boolean, nie napis. Wystarczy if (cbSer.isChecked). |
| cena liczy tylko ostatnio kliknięty dodatek | Każdy listener dolicza do wspólnej sumy zamiast liczyć wszystko od nowa. Licz w jednej funkcji od ceny bazowej. |
| suma rośnie przy każdym kliknięciu, także przy odznaczaniu | Listener dodaje cenę bez sprawdzenia stanu. Sprawdzaj isChecked. |
| przycisk „Wyczyść” odznacza, ale cena się nie zmienia | Użyto setOnClickListener na polach — nie reaguje na zmiany z kodu. |
zmienna cena poza funkcją, a przelicz() tylko dodaje | Cena rośnie bez końca. Zmienną liczoną od nowa deklaruj wewnątrz funkcji. |
pola wyboru w RadioGroup | RadioGroup obsługuje tylko RadioButton (lekcja 10). CheckBoksy wkładaj w zwykły układ. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Trzy pola: „Akceptuję regulamin” (wymagane), „Zgoda na newsletter”, „Mam ukończone 16 lat” (wymagane). Przycisk Załóż konto jest aktywny tylko wtedy, gdy oba wymagane pola są zaznaczone.
pokaż wskazówkę
- Funkcja
sprawdz():btn.isEnabled = cb1.isChecked && cb3.isChecked, wywoływana z listenerów obu wymaganych pól.
Lista pięciu zadań domowych jako pola wyboru i nad nimi pole „Zaznacz wszystkie”. Jego zaznaczenie zaznacza wszystkie zadania, odznaczenie — odznacza. Pod listą: „Zrobione: 3 z 5”.
pokaż wskazówkę
- W listenerze „Zaznacz wszystkie” ustaw
isChecked = zaznaczonykażdemu z pięciu pól.
Osiem zainteresowań (sport, muzyka, gry, książki, film, programowanie, podróże, gotowanie). Użytkownik może wybrać najwyżej trzy: po zaznaczeniu trzeciego pozostałe pola się wyłączają, po odznaczeniu — znów włączają. Na dole „Wybrano: gry, programowanie (2/3)”.
pokaż wskazówkę
- Trzymaj pola w tablicy
val pola = arrayOf(cb1, …, cb8). Liczba zaznaczonych:pola.count { it.isChecked }. Wyłączanie w pętli: niezaznaczone pole dostajeisEnabled = ile < 3.
Pole tekstowe z liczbą uczestników i pola wyboru opcji: autokar (60 zł/os.), bilety do muzeum (25 zł/os.), obiad (35 zł/os.), przewodnik (300 zł za całą grupę), ubezpieczenie (8 zł/os.). Koszt całkowity i na osobę przeliczają się przy każdej zmianie pola wyboru i po kliknięciu Przelicz.
pokaż wskazówkę
- W
przelicz()odczytaj liczbę osób przeztoIntOrNull()(lekcja 06); gdy jej brak — pokaż komunikat i zakończ funkcję przezreturn.
Sześć punktów do odhaczenia przed sprawdzianem z programowania. Pasek z napisem „Gotowość: 50%” zmienia kolor tła: czerwony poniżej 50%, pomarańczowy 50–99%, zielony przy 100%. Przy 100% pojawia się „Powodzenia!”.
pokaż wskazówkę
- Procent:
ile * 100 / 6. Kolor tła napisu zmienisz przezsetBackgroundColor(lekcja 04).
Laptop bazowy 2800 zł. Opcje: dodatkowe 16 GB RAM (+400), dysk 1 TB zamiast 512 GB (+350), ekran OLED (+900), podświetlana klawiatura (+150), przedłużona gwarancja (+10% ceny wszystkich pozostałych elementów), torba (+120). Ekran OLED wymaga dodatkowego RAM — zaznaczenie OLED samo zaznacza RAM, a odznaczenie RAM odznacza OLED. Podsumowanie: lista wybranych opcji z cenami w osobnych liniach i suma.
pokaż wskazówkę
- Gwarancję policz na końcu
przelicz(), od sumy reszty. Zależność OLED ↔ RAM zapisz w dwóch listenerach:if (zaznaczony) cbRam.isChecked = trueorazif (!zaznaczony) cbOled.isChecked = false.
Co trzeba zapamiętać
CheckBoxsłuży do wyboru dowolnie wielu opcji; każde pole działa niezależnie.- Stan:
isChecked— odczyt i zmiana; na starcieandroid:checked. - Na zmianę reaguj przez
setOnCheckedChangeListener— działa także przy zmianach z kodu. - Wynik zależny od wielu pól licz w jednej funkcji, zawsze od początku.
- Przycisk zależny od zgody:
btn.isEnabled = zaznaczony.
Dokumentacja: Pola wyboru.