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

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.

Kotlin setItems setSingleChoiceItems setMultiChoiceItems BooleanArray copyOf 2 × 45 min
CEL LEKCJI

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.
TEORIA

Trzy rodzaje list

12:30
Wybierz kawę
Espresso
Latte
Cappuccino
Flat white
Americano
setItems
wybór od razu zamyka okno
12:30
Rozmiar
S — 250 ml
M — 350 ml (+2 zł)
L — 450 ml (+4 zł)
AnulujOK
setSingleChoiceItems
jeden wybór + OK
12:30
Dodatki
Syrop karmelowy (+2 zł)
Bita śmietana (+2 zł)
Podwójne espresso (+3 zł)
Mleko owsiane (+2 zł)
AnulujOK
setMultiChoiceItems
wiele wyborów + OK
MetodaZamyka się po wyborze?Kiedy
setItemstak, od razuSzybki wybór akcji: „Zrób zdjęcie / Wybierz z galerii / Usuń”.
setSingleChoiceItemsnie — potrzebny przycisk OKZmiana ustawienia z widoczną obecną wartością.
setMultiChoiceItemsnie — potrzebny przycisk OKWybó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.

TEORIA

Zwykła lista: setItems

wybór kawy
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).

TEORIA

Jeden wybór: setSingleChoiceItems

wybór rozmiaru z zaznaczonym obecnym
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.

TEORIA

Wiele wyborów: setMultiChoiceItems

Stan pól wyboru w oknie przechowuje tablica BooleanArray — po jednej wartości na pozycję listy, jak w lekcji 08:

wybór dodatków
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).

TEORIA

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.

przed oknem
robocza = wybraneDodatki.copyOf()
→
w oknie
klikanie zmienia tylko kopię
→
OK
wybraneDodatki = robocza
lub
Anuluj
kopia wyrzucona,
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).

PRZYKŁAD

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.

8:05
Kawiarnia Pod Serwerem
Kawa: Latte
Rozmiar: M — 350 ml
Dodatki: 2
Latte, M
+ syrop karmelowy
+ podwójne espresso
Razem: 21 zł
Zamów
zamówienie po trzech oknach

latte 14 zł + rozmiar M 2 zł + syrop 2 zł + espresso 3 zł = 21 zł

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Kawiarnia, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt.
res/layout/activity_main.xml — pełny plik
<?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>
MainActivity.kt — pełny plik
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 tablica BooleanArray z 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, a first() bierze pierwszą literę (S, M, L).

Sprawdź trzy rzeczy

  1. Otwórz dodatki, zaznacz dwa i kliknij „Anuluj”. Liczba dodatków się nie zmieni.
  2. Zmień w wybierzDodatki robocza na wybraneDodatki w setMultiChoiceItems i powtórz. Teraz „Anuluj” zostawia zmiany — choć podsumowanie odświeży się dopiero przy następnym oknie.
  3. Dopisz w oknie kawy .setMessage("Ceny od 9 zł"). Lista zniknie — zostanie sam tekst.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
.setItems(tablica) { _, ktory -> }metodaLista; wybór zamyka okno.
.setSingleChoiceItems(tablica, zaznaczony) { _, ktory -> }metodaLista jednokrotnego wyboru; -1 — nic na starcie.
.setMultiChoiceItems(tablica, booleany) { _, ktory, zaznaczony -> }metodaLista wielokrotnego wyboru; zmienia podaną tablicę.
BooleanArray(n)tablican wartości false.
tablica.copyOf()tablicaNowa tablica z tymi samymi wartościami.
tablica.count { it }IntLiczba wartości true.
napis.substringBefore(" (")StringCzęść napisu przed podanym fragmentem.
napis.first()CharPierwszy znak.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
lista się nie pokazuje, widać tylko tekstW oknie jest setMessage. Usuń je — z listą zostaje tylko tytuł.
„Anuluj” w liście wielokrotnego wyboru nic nie cofaOkno dostało oryginalną tablicę. Podawaj copyOf().
jeden wybór bez przycisku OKOkno 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 pozycjiAwaria ArrayIndexOutOfBoundsException. Tablica stanów musi mieć tyle elementów co lista.
lambda setMultiChoiceItems z dwoma parametramiBłąd kompilacji — ta lambda ma trzy: { _, ktory, zaznaczony -> }.
ZADANIA

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.

ZAD 1Zdjęcie profilowe★☆☆

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 -> … }.
ZAD 2Wybór języka★☆☆

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.
ZAD 3Filtr ogłoszeń★★☆

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]] jest true.
ZAD 4Zaznacz wszystko w oknie★★☆

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.
ZAD 5Quiz w oknach★★☆

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). Metoda pokazPytanie(nr: Int) w lambdzie Dalej wywołuje pokazPytanie(nr + 1), a po ostatnim — pokazWynik(). Brak wyboru (-1) w Dalej: pokaż to samo pytanie jeszcze raz.
ZAD 6Pizza w oknach★★★

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 }.
PODSUMOWANIE

Co trzeba zapamiętać

  • setItems — wybór zamyka okno; setSingleChoiceItems i setMultiChoiceItems — 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ą.

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