school-it.pl // android + kotlin // rozdział 6 // lekcja 23
AlertDialog z listą wyboru
Okno dialogowe może zamiast treści pokazać listę opcji. Są trzy rodzaje takich list i odpowiadają znanym ci kontrolkom: zwykła lista (wybór od razu zamyka okno), lista z przyciskami opcji (jak RadioButton — jeden wybór) i lista z polami wyboru (jak CheckBox — wiele wyborów). Wszystkie trzy wykorzystasz w aplikacji do zamawiania kawy.
Czego się dziś nauczysz
- Pokażesz w oknie listę, z której wybór od razu zamyka okno.
- Pokażesz listę jednokrotnego wyboru z zaznaczeniem bieżącej opcji.
- Pokażesz listę wielokrotnego wyboru i odczytasz wszystkie zaznaczenia.
- Sprawisz, że „Anuluj” naprawdę cofa zmiany zrobione w oknie.
- Dobierzesz rodzaj listy do sytuacji.
Trzy rodzaje list
Latte
Cappuccino
Flat white
Americano
wybór od razu zamyka okno
jeden wybór + OK
wiele wyborów + OK
| Metoda | Zamyka się po wyborze? | Kiedy |
|---|---|---|
setItems | tak, od razu | Szybki wybór akcji: „Zrób zdjęcie / Wybierz z galerii / Usuń”. |
setSingleChoiceItems | nie — potrzebny przycisk OK | Zmiana ustawienia z widoczną obecną wartością. |
setMultiChoiceItems | nie — potrzebny przycisk OK | Wybór kilku rzeczy naraz. |
Lista albo treść — nie jedno i drugie
Gdy w oknie jest setMessage, lista się nie pokaże. Okno z listą ma tylko tytuł (i ewentualnie przyciski). Wyjaśnienie umieść w tytule.
Zwykła lista: setItems
val kawy = arrayOf("Espresso", "Latte", "Cappuccino", "Flat white", "Americano")
AlertDialog.Builder(this)
.setTitle("Wybierz kawę")
.setItems(kawy) { _, ktory ->
tvKawa.text = kawy[ktory] // ktory = numer klikniętej pozycji od 0
}
.show()
Lambda dostaje okno i numer klikniętej pozycji. Numer pasuje do indeksu w tablicy, z której zbudowano listę — tak samo jak position w spinnerze (lekcja 12).
Jeden wybór: setSingleChoiceItems
var tymczasowy = rozmiar // wybór w oknie, jeszcze niezatwierdzony
AlertDialog.Builder(this)
.setTitle("Rozmiar")
.setSingleChoiceItems(rozmiary, rozmiar) { _, ktory ->
tymczasowy = ktory // kliknięcie tylko zmienia zaznaczenie
}
.setPositiveButton("OK") { _, _ ->
rozmiar = tymczasowy // dopiero OK zatwierdza
pokazZamowienie()
}
.setNegativeButton("Anuluj", null)
.show()
Drugi parametr setSingleChoiceItems to numer opcji zaznaczonej na starcie (-1 — żadnej). Kliknięcie opcji nie zamyka okna — użytkownik może zmienić zdanie. Dlatego wybór zapamiętujemy w zmiennej tymczasowej i przepisujemy do pola dopiero po OK.
Wiele wyborów: setMultiChoiceItems
Stan pól wyboru w oknie przechowuje tablica BooleanArray — po jednej wartości na pozycję listy, jak w lekcji 08:
val robocza = wybraneDodatki.copyOf() // kopia — patrz „Anuluj musi anulować”
AlertDialog.Builder(this)
.setTitle("Dodatki")
.setMultiChoiceItems(dodatki, robocza) { _, ktory, zaznaczony ->
robocza[ktory] = zaznaczony
}
.setPositiveButton("OK") { _, _ ->
wybraneDodatki = robocza
pokazZamowienie()
}
.setNegativeButton("Anuluj", null)
.show()
Lambda dostaje trzy wartości: okno, numer pozycji i jej nowy stan (true — zaznaczona).
Anuluj musi anulować
Okno wielokrotnego wyboru samo zmienia tablicę, którą mu podasz — przy każdym kliknięciu pola. Gdybyś podał mu bezpośrednio pole wybraneDodatki, kliknięcie „Anuluj” nic by nie cofnęło: tablica byłaby już zmieniona.
pole bez zmian
copyOf() tworzy nową tablicę z tymi samymi wartościami. Ta sama zasada — „pracuj na kopii, zatwierdź na OK” — dotyczy listy jednokrotnego wyboru (zmienna tymczasowy).
Kawiarnia
Zamówienie kawy w trzech oknach: rodzaj kawy (zwykła lista), rozmiar (jeden wybór, z zaznaczonym obecnym) i dodatki (wiele wyborów, z poprawnie działającym „Anuluj”). Podsumowanie z ceną aktualizuje się po każdym zatwierdzeniu. Zamów działa dopiero po wybraniu kawy.
+ syrop karmelowy
+ podwójne espresso
latte 14 zł + rozmiar M 2 zł + syrop 2 zł + espresso 3 zł = 21 zł
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Kawiarnia, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="16dp"
android:fitsSystemWindows="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Kawiarnia Pod Serwerem"
android:textSize="24sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnKawa"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnRozmiar"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnDodatki"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tvZamowienie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="#F3EDF7"
android:lineSpacingExtra="4dp"
android:padding="16dp"
android:textSize="16sp" />
<Button
android:id="@+id/btnZamow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Zamów" />
</LinearLayout>
package pl.schoolit.kawiarnia
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var btnKawa: Button
private lateinit var btnRozmiar: Button
private lateinit var btnDodatki: Button
private lateinit var btnZamow: Button
private lateinit var tvZamowienie: TextView
// ---------- dane (równoległe tablice) ----------
private val kawy = arrayOf("Espresso", "Latte", "Cappuccino", "Flat white", "Americano")
private val cenyKaw = intArrayOf(9, 14, 13, 15, 10)
private val rozmiary = arrayOf("S — 250 ml", "M — 350 ml (+2 zł)", "L — 450 ml (+4 zł)")
private val doplatyRozmiar = intArrayOf(0, 2, 4)
private val dodatki = arrayOf("Syrop karmelowy (+2 zł)", "Bita śmietana (+2 zł)",
"Podwójne espresso (+3 zł)", "Mleko owsiane (+2 zł)")
private val nazwyDodatkow = arrayOf("syrop karmelowy", "bita śmietana", "podwójne espresso", "mleko owsiane")
private val cenyDodatkow = intArrayOf(2, 2, 3, 2)
// ---------- stan zamówienia ----------
private var kawa = -1 // -1 = jeszcze nie wybrano
private var rozmiar = 0 // domyślnie S
private var wybraneDodatki = BooleanArray(dodatki.size) // wszystkie false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
ustawZdarzenia()
pokazZamowienie()
}
private fun initUI() {
btnKawa = findViewById(R.id.btnKawa)
btnRozmiar = findViewById(R.id.btnRozmiar)
btnDodatki = findViewById(R.id.btnDodatki)
btnZamow = findViewById(R.id.btnZamow)
tvZamowienie = findViewById(R.id.tvZamowienie)
}
private fun ustawZdarzenia() {
btnKawa.setOnClickListener { wybierzKawe() }
btnRozmiar.setOnClickListener { wybierzRozmiar() }
btnDodatki.setOnClickListener { wybierzDodatki() }
btnZamow.setOnClickListener {
Toast.makeText(this, "Zamówienie przyjęte!", Toast.LENGTH_SHORT).show()
}
}
// ---------- okna ----------
// 1. zwykła lista: wybór od razu zamyka okno
private fun wybierzKawe() {
AlertDialog.Builder(this)
.setTitle("Wybierz kawę")
.setItems(kawy) { _, ktory ->
kawa = ktory
pokazZamowienie()
}
.show()
}
// 2. jeden wybór: zaznaczony obecny rozmiar, zatwierdzenie przyciskiem OK
private fun wybierzRozmiar() {
var tymczasowy = rozmiar
AlertDialog.Builder(this)
.setTitle("Rozmiar")
.setSingleChoiceItems(rozmiary, rozmiar) { _, ktory ->
tymczasowy = ktory
}
.setPositiveButton("OK") { _, _ ->
rozmiar = tymczasowy
pokazZamowienie()
}
.setNegativeButton("Anuluj", null)
.show()
}
// 3. wiele wyborów: praca na kopii, zatwierdzenie przyciskiem OK
private fun wybierzDodatki() {
val robocza = wybraneDodatki.copyOf()
AlertDialog.Builder(this)
.setTitle("Dodatki")
.setMultiChoiceItems(dodatki, robocza) { _, ktory, zaznaczony ->
robocza[ktory] = zaznaczony
}
.setPositiveButton("OK") { _, _ ->
wybraneDodatki = robocza
pokazZamowienie()
}
.setNegativeButton("Anuluj", null)
.show()
}
// ---------- stan → ekran ----------
private fun pokazZamowienie() {
btnKawa.text = if (kawa == -1) "Wybierz kawę" else "Kawa: ${kawy[kawa]}"
btnRozmiar.text = "Rozmiar: " + rozmiary[rozmiar].substringBefore(" (")
btnDodatki.text = "Dodatki: ${wybraneDodatki.count { it }}"
btnZamow.isEnabled = kawa != -1
if (kawa == -1) {
tvZamowienie.text = "Zacznij od wyboru kawy."
return
}
var cena = cenyKaw[kawa] + doplatyRozmiar[rozmiar]
var opis = "${kawy[kawa]}, ${rozmiary[rozmiar].first()}"
for (i in dodatki.indices) {
if (wybraneDodatki[i]) {
cena += cenyDodatkow[i]
opis += "\n+ ${nazwyDodatkow[i]}"
}
}
tvZamowienie.text = "$opis\n\nRazem: $cena zł"
}
}
Co dzieje się po kolei
- Dane są w równoległych tablicach (lekcja 12): nazwy do wyświetlenia w oknach i liczby do obliczeń.
- Stan zamówienia to trzy pola: numer kawy (
-1= brak), numer rozmiaru i tablicaBooleanArrayz dodatkami. - Okno kawy (
setItems) zamyka się po kliknięciu i od razu zmienia stan. - Okno rozmiaru startuje z zaznaczonym obecnym rozmiarem. Kliknięcia zmieniają tylko zmienną
tymczasowy; dopiero OK przepisuje ją do pola. - Okno dodatków dostaje kopię tablicy. „Anuluj” wyrzuca kopię, OK zastępuje nią pole.
pokazZamowienie()odświeża napisy na przyciskach i podsumowanie.substringBefore(" (")ucina dopisek o dopłacie, afirst()bierze pierwszą literę (S, M, L).
Sprawdź trzy rzeczy
- Otwórz dodatki, zaznacz dwa i kliknij „Anuluj”. Liczba dodatków się nie zmieni.
- Zmień w
wybierzDodatkiroboczanawybraneDodatkiwsetMultiChoiceItemsi powtórz. Teraz „Anuluj” zostawia zmiany — choć podsumowanie odświeży się dopiero przy następnym oknie. - Dopisz w oknie kawy
.setMessage("Ceny od 9 zł"). Lista zniknie — zostanie sam tekst.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
.setItems(tablica) { _, ktory -> } | metoda | Lista; wybór zamyka okno. |
.setSingleChoiceItems(tablica, zaznaczony) { _, ktory -> } | metoda | Lista jednokrotnego wyboru; -1 — nic na starcie. |
.setMultiChoiceItems(tablica, booleany) { _, ktory, zaznaczony -> } | metoda | Lista wielokrotnego wyboru; zmienia podaną tablicę. |
BooleanArray(n) | tablica | n wartości false. |
tablica.copyOf() | tablica | Nowa tablica z tymi samymi wartościami. |
tablica.count { it } | Int | Liczba wartości true. |
napis.substringBefore(" (") | String | Część napisu przed podanym fragmentem. |
napis.first() | Char | Pierwszy znak. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| lista się nie pokazuje, widać tylko tekst | W oknie jest setMessage. Usuń je — z listą zostaje tylko tytuł. |
| „Anuluj” w liście wielokrotnego wyboru nic nie cofa | Okno dostało oryginalną tablicę. Podawaj copyOf(). |
| jeden wybór bez przycisku OK | Okno się nie zamknie po kliknięciu opcji — użytkownik musi zamknąć je Wstecz, a wybór przepadnie. Dodaj setPositiveButton. |
zmiana stanu w lambdzie setSingleChoiceItems | „Anuluj” nie cofa wyboru. Zmieniaj zmienną tymczasową, pole dopiero w OK. |
setMultiChoiceItems(dodatki, BooleanArray(3)) dla 4 pozycji | Awaria ArrayIndexOutOfBoundsException. Tablica stanów musi mieć tyle elementów co lista. |
lambda setMultiChoiceItems z dwoma parametrami | Błąd kompilacji — ta lambda ma trzy: { _, ktory, zaznaczony -> }. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w układzie z lekcji 19. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Okrągły awatar (lekcja 17). Dotknięcie otwiera okno z listą: „Zmień kolor”, „Zmień inicjały na losowe”, „Przywróć domyślny”. Każda pozycja od razu wykonuje akcję.
pokaż wskazówkę
setItems+when (ktory) { 0 -> … 1 -> … }.
Przycisk „Język: polski” otwiera okno jednokrotnego wyboru z pięcioma językami i zaznaczonym obecnym. Po OK napis przycisku i przywitanie na ekranie zmieniają się na wybrany język („Cześć!”, „Hello!”, „Hallo!”…).
pokaż wskazówkę
- Dwie równoległe tablice: nazwy języków i powitania. Wzór ze zmienną tymczasową jak w przykładzie.
Lista ośmiu ogłoszeń szkolnych w jednym TextView, każde z kategorią (sport, nauka, wydarzenia, ogłoszenia dyrekcji). Przycisk Filtruj otwiera okno wielokrotnego wyboru kategorii; po OK widać tylko ogłoszenia z zaznaczonych kategorii. Odznaczenie wszystkich pokazuje „Brak ogłoszeń”.
pokaż wskazówkę
- Ogłoszenia trzymaj w dwóch tablicach: treść i numer kategorii. Pokazuj tylko te, dla których
filtr[kategoria[i]]jesttrue.
Okno wielokrotnego wyboru z sześcioma przedmiotami do powtórki i trzecim przyciskiem (neutralnym) Wszystkie, który zaznacza wszystkie pozycje i zatwierdza wybór.
pokaż wskazówkę
- Lambda przycisku neutralnego:
wybrane = BooleanArray(przedmioty.size) { true }— tablica wypełniona wartościątrue.
Pięć pytań, każde w oknie jednokrotnego wyboru (cztery odpowiedzi, brak zaznaczenia na starcie, przycisk Dalej). Okno nie może się zamknąć bez odpowiedzi. Po ostatnim pytaniu okno z wynikiem „4/5”.
pokaż wskazówkę
setCancelable(false). MetodapokazPytanie(nr: Int)w lambdzie Dalej wywołujepokazPytanie(nr + 1), a po ostatnim —pokazWynik(). Brak wyboru (-1) w Dalej: pokaż to samo pytanie jeszcze raz.
Przerób zamówienie pizzy z lekcji 09 tak, aby ekran miał tylko trzy przyciski i podsumowanie: rodzaj pizzy (zwykła lista), rozmiar (jeden wybór: 30/40/50 cm z dopłatą) i dodatki (wiele wyborów, najwyżej 4 — przy próbie zatwierdzenia większej liczby pojawia się okno z komunikatem i wybór nie jest zapisywany). Rabat od 3 dodatków zostaje.
pokaż wskazówkę
- Limit sprawdzaj w lambdzie OK:
if (robocza.count { it } > 4) { pokazBlad(); return@setPositiveButton }.
Co trzeba zapamiętać
setItems— wybór zamyka okno;setSingleChoiceItemsisetMultiChoiceItems— zatwierdzenie przyciskiem OK.- Okno z listą nie może mieć
setMessage. - Numer pozycji w lambdzie to indeks w tablicy, z której zbudowano listę.
- Pracuj na kopii (zmienna tymczasowa,
copyOf()), a zatwierdzaj dopiero w OK — wtedy „Anuluj” naprawdę anuluje. - Tablica stanów dla wielokrotnego wyboru ma tyle elementów co lista.
Dokumentacja: Okna dialogowe z listą.