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

school-it.pl // android + kotlin // rozdział 3 // lekcja 12

Spinner — lista rozwijana

Gdy opcji jest dużo — waluty, kraje, klasy w szkole — grupa przycisków opcji zajęłaby cały ekran. Spinner pokazuje tylko wybraną pozycję, a resztę rozwija po dotknięciu. W tej lekcji wypełnisz listę rozwijaną na dwa sposoby: z pliku zasobów i z kodu przez ArrayAdapter, a potem zbudujesz przelicznik walut z dwiema listami.

Kotlin XML Spinner android:entries ArrayAdapter onItemSelectedListener 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wypełnisz listę rozwijaną tablicą z strings.xml.
  • Wypełnisz ją z kodu za pomocą ArrayAdapter.
  • Odczytasz wybraną pozycję i jej numer.
  • Zareagujesz na zmianę wyboru obiektem OnItemSelectedListener.
  • Ustawisz wybór z kodu metodą setSelection.
TEORIA

Spinner z zasobów

Najprostszy sposób: lista pozycji w strings.xml jako string-array i atrybut android:entries. Nie trzeba ani linii kodu, żeby lista się wyświetliła:

res/values/strings.xml — fragment
<string-array name="klasy">
    <item>1TP</item>
    <item>2TP</item>
    <item>3TP</item>
    <item>4TP</item>
    <item>5TP</item>
</string-array>
w układzie
<Spinner
    android:id="@+id/spKlasa"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:entries="@array/klasy" />
12:30
Klasa
3TP
zwinięty — widać wybraną pozycję
→dotknięcie
12:30
Klasa
1TP
2TP
3TP
4TP
5TP
rozwinięty — wybór jednej pozycji

po wybraniu pozycji lista sama się zwija

TEORIA

Spinner z kodu: ArrayAdapter

Gdy pozycje powstają w kodzie (np. są wyliczane albo zależą od innego wyboru), lista potrzebuje adaptera. Adapter to pośrednik: bierze tablicę napisów i z każdego napisu robi wiersz listy. Spinner sam nie wie, jak wyświetlić twoje dane — adapter mu to podaje.

MainActivity.kt — fragment onCreate
val dni = arrayOf("Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek")

val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, dni)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spDzien.adapter = adapter
FragmentZnaczenie
ArrayAdapter(this, …, dni)Adapter dla tablicy dni w tej aktywności.
android.R.layout.simple_spinner_itemWbudowany wygląd pozycji widocznej, gdy lista jest zwinięta.
setDropDownViewResource(…dropdown_item)Wbudowany wygląd pozycji na rozwiniętej liście — wyższy, wygodny dla palca.
spDzien.adapter = adapterPodłączenie adaptera do listy.

Przedrostek android.R oznacza zasoby wbudowane w system, a nie twoje. Ten sam ArrayAdapter poznasz dokładniej w lekcji 25 — tam wypełnia przewijaną listę ListView.

TEORIA

Odczyt wyboru

ZapisWynik
sp.selectedItem.toString()Tekst wybranej pozycji, np. „Środa”.
sp.selectedItemPositionNumer wybranej pozycji, od 0 (Środa = 2).
sp.setSelection(4)Wybiera pozycję nr 4 z kodu (Piątek).
sp.countLiczba pozycji na liście.

Numer pozycji jest wygodniejszy niż tekst: pasuje do indeksu w tablicy, więc z równoległej tablicy można od razu wziąć związaną z nim wartość — na przykład kurs waluty.

TEORIA

Reakcja na wybór

Spinner nie ma prostego setOnClickListener dla wyboru. Zamiast tego dostaje obiekt słuchacza z dwiema funkcjami: co zrobić po wybraniu pozycji i co zrobić, gdy nic nie jest wybrane. Taki obiekt tworzy się zapisem object : …:

słuchacz wyboru
spDzien.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {

    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        tvInfo.text = "Wybrano: ${dni[position]}"
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        // zwykle pusto — spinner prawie zawsze ma wybraną pozycję
    }
}

Nie musisz pisać tych dwóch funkcji z pamięci. Po wpisaniu object : AdapterView.OnItemSelectedListener {} Android Studio podkreśli kod — Alt+Enter → Implement members doda je sam. Z czterech parametrów onItemSelected najważniejszy jest position — numer wybranej pozycji.

onItemSelected wywoła się od razu

Zaraz po starcie ekranu spinner „wybiera” pozycję nr 0 i wywołuje onItemSelected, choć użytkownik niczego nie dotknął. Zwykle to zaleta — wynik od razu jest policzony. Pamiętaj o tym, gdy listener ma pokazywać komunikat w rodzaju „Zmieniłeś wybór”.

PRZYKŁAD

Przelicznik walut

Kwota w polu tekstowym i dwie listy rozwijane: z jakiej waluty i na jaką. Wynik przelicza się po każdej zmianie listy i po kliknięciu Przelicz. Przycisk ⇅ zamienia waluty miejscami. Kursy są wpisane w kod jako przykładowe — nie są aktualne.

10:15
Przelicznik walut
250
Z waluty
PLN — złoty polski
⇅
Na walutę
EUR — euro
Przelicz
250,00 PLN = 58,14 EUR
kurs przykładowy: 1 EUR = 4,30 PLN
złote na euro
→⇅
10:15
Przelicznik walut
250
Z waluty
EUR — euro
⇅
Na walutę
PLN — złoty polski
Przelicz
250,00 EUR = 1075,00 PLN
kurs przykładowy: 1 EUR = 4,30 PLN
po zamianie walut

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Waluty, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt. Listy są wypełniane z kodu, bo nazwa i kurs waluty muszą mieć ten sam numer.
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:text="Przelicznik walut"
        android:textSize="24sp"
        android:textStyle="bold" />

    <EditText
        android:id="@+id/etKwota"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:hint="Kwota"
        android:inputType="numberDecimal" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Z waluty" />

    <Spinner
        android:id="@+id/spZ"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="48dp" />

    <Button
        android:id="@+id/btnZamien"
        style="@style/Widget.Material3.Button.TextButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="Zamień waluty"
        android:text="⇅"
        android:textSize="22sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Na walutę" />

    <Spinner
        android:id="@+id/spNa"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="48dp" />

    <Button
        android:id="@+id/btnPrzelicz"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:text="Przelicz" />

    <TextView
        android:id="@+id/tvWynik"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="22sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tvKurs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp" />

</LinearLayout>
MainActivity.kt — pełny plik
package pl.schoolit.waluty

import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
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 etKwota = findViewById<EditText>(R.id.etKwota)
        val spZ = findViewById<Spinner>(R.id.spZ)
        val spNa = findViewById<Spinner>(R.id.spNa)
        val btnZamien = findViewById<Button>(R.id.btnZamien)
        val btnPrzelicz = findViewById<Button>(R.id.btnPrzelicz)
        val tvWynik = findViewById<TextView>(R.id.tvWynik)
        val tvKurs = findViewById<TextView>(R.id.tvKurs)

        // trzy tablice tej samej długości: pozycja nr 1 w każdej to euro
        val nazwy = arrayOf("PLN — złoty polski", "EUR — euro", "USD — dolar amerykański",
                            "GBP — funt brytyjski", "CZK — korona czeska")
        val kody = arrayOf("PLN", "EUR", "USD", "GBP", "CZK")
        val kursy = doubleArrayOf(1.0, 4.30, 3.95, 5.10, 0.17)   // ile PLN kosztuje 1 jednostka — przykładowe!

        // jeden adapter wystarczy dla obu list
        val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, nazwy)
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        spZ.adapter = adapter
        spNa.adapter = adapter
        spNa.setSelection(1)            // na start: PLN → EUR

        fun przelicz() {
            val kwota = etKwota.text.toString().trim().replace(',', '.').toDoubleOrNull()
            if (kwota == null || kwota < 0) {
                tvWynik.text = "Wpisz kwotę"
                tvKurs.text = ""
                return
            }

            val z = spZ.selectedItemPosition
            val na = spNa.selectedItemPosition

            // przez złotówki: kwota → PLN → waluta docelowa
            val wynik = kwota * kursy[z] / kursy[na]

            tvWynik.text = "${String.format("%.2f", kwota)} ${kody[z]} = ${String.format("%.2f", wynik)} ${kody[na]}"
            tvKurs.text = "kurs przykładowy: 1 ${kody[z]} = ${String.format("%.4f", kursy[z] / kursy[na])} ${kody[na]}"
        }

        // jeden słuchacz dla obu list
        val sluchacz = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                przelicz()
            }

            override fun onNothingSelected(parent: AdapterView<*>?) {
            }
        }
        spZ.onItemSelectedListener = sluchacz
        spNa.onItemSelectedListener = sluchacz

        btnPrzelicz.setOnClickListener {
            przelicz()
        }

        btnZamien.setOnClickListener {
            val z = spZ.selectedItemPosition
            spZ.setSelection(spNa.selectedItemPosition)
            spNa.setSelection(z)
        }
    }
}

Co dzieje się po kolei

  • Trzy tablice są „równoległe”: element o numerze 3 we wszystkich opisuje funta. Numer wybranej pozycji spinnera jest więc od razu indeksem kursu i kodu.
  • Oba spinnery dostają ten sam adapter — pokazują te same waluty. spNa.setSelection(1) ustawia euro jako walutę docelową.
  • Kurs to cena jednej jednostki w złotych. Przeliczenie idzie przez złotówki: 250 EUR × 4,30 = 1075 PLN, a 1075 PLN ÷ 1,0 = 1075 PLN.
  • Jeden obiekt sluchacz obsługuje oba spinnery — w obu przypadkach trzeba policzyć to samo.
  • ⇅ zapamiętuje numer z pierwszej listy w zmiennej, ustawia pierwszą listę na wybór drugiej, a drugą na zapamiętany numer. Oba setSelection wywołują słuchacza, więc wynik przelicza się sam.
  • Zmiana samej kwoty nie przelicza wyniku — do tego jest przycisk Przelicz.

Sprawdź trzy rzeczy

  1. Uruchom aplikację z pustym polem. Od razu pojawi się „Wpisz kwotę” — onItemSelected wywołał się przy starcie.
  2. Ustaw tę samą walutę na obu listach. Kurs wyniesie 1,0000, a kwota się nie zmieni.
  3. Usuń linię z setDropDownViewResource. Rozwinięta lista będzie miała niskie, ciasne pozycje, w które trudno trafić palcem.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<Spinner>kontrolkaLista rozwijana — jedna wybrana pozycja.
android:entries="@array/…"atrybutPozycje z string-array.
ArrayAdapter(this, układ, tablica)klasaZamienia tablicę napisów na pozycje listy.
android.R.layout.simple_spinner_itemzasób systemuWygląd pozycji zwiniętej.
android.R.layout.simple_spinner_dropdown_itemzasób systemuWygląd pozycji rozwiniętej.
sp.adapter = adapterwłaściwośćPodłącza adapter.
sp.selectedItemPositionIntNumer wybranej pozycji od 0.
sp.selectedItemobiektWybrana pozycja — z .toString() jako tekst.
sp.setSelection(nr)metodaWybór z kodu.
object : AdapterView.OnItemSelectedListener { … }obiektSłuchacz z onItemSelected i onNothingSelected.
doubleArrayOf(…)funkcjaTablica liczb Double.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
sp.setOnClickListener { … }Awaria: Don’t call setOnClickListener for an AdapterView. Użyj onItemSelectedListener.
sp.setOnItemClickListenerTeż awaria — to metoda dla ListView, nie dla spinnera.
błąd przy object : AdapterView.OnItemSelectedListenerObject is not abstract and does not implement… — brakuje jednej z dwóch funkcji. Alt+Enter → Implement members.
R.layout.simple_spinner_itemUnresolved reference — zasób systemowy: android.R.layout….
lista pustaBrak sp.adapter = adapter albo pusta tablica.
tablice nazw i kursów różnej długościPrzy ostatniej pozycji awaria ArrayIndexOutOfBoundsException. Równoległe tablice muszą mieć tyle samo elementów.
komunikat „Zmieniono wybór” zaraz po starcieonItemSelected wywołuje się przy starcie dla pozycji 0.
ZADANIA

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.

ZAD 1Wybór klasy★☆☆

Spinner z klasami 1TP–5TP z strings.xml (android:entries) i przycisk Pokaż, który wypisuje „Wybrano klasę 3TP (pozycja 2)”.

pokaż wskazówkę
  • sp.selectedItem.toString() i sp.selectedItemPosition.
ZAD 2Plan lekcji★☆☆

Spinner z dniami tygodnia (z kodu). Wybór dnia od razu pokazuje pod spodem plan lekcji na ten dzień (wymyślony, po 5–7 lekcji w osobnych liniach).

pokaż wskazówkę
  • Plany trzymaj w tablicy napisów z \n między lekcjami; indeks z position.
ZAD 3Przelicznik jednostek★★☆

Jak przelicznik walut, ale dla długości: mm, cm, m, km, cal, stopa, mila. Wynik przelicza się po zmianie list i po przycisku. Liczby bardzo małe i bardzo duże wypisuj w czytelny sposób (np. 6 cyfr znaczących).

pokaż wskazówkę
  • Zamiast kursów do złotówki — ile metrów ma jednostka. Format: String.format("%.6g", wynik).
ZAD 4Zamówienie kawy★★☆

Trzy spinnery: rodzaj kawy (espresso 8 zł, latte 12 zł, cappuccino 11 zł, flat white 13 zł), rozmiar (S, M +2 zł, L +4 zł), mleko (krowie, owsiane +2 zł, sojowe +2 zł, bez mleka). Dla espresso spinner mleka ustawia się na „bez mleka” i wyłącza. Cena aktualizuje się po każdej zmianie.

pokaż wskazówkę
  • Jeden słuchacz dla trzech spinnerów. Wyłączenie: spMleko.isEnabled = false i setSelection na pozycję „bez mleka”.
ZAD 5Data z trzech list★★☆

Trzy spinnery: dzień (1–31), miesiąc (nazwy) i rok (2000–2030), wypełnione z kodu w pętli. Po każdej zmianie pod spodem: „15 marca 2026” albo — gdy data nie istnieje (31 kwietnia, 29 lutego w roku nieprzestępnym) — „Taka data nie istnieje” na czerwono.

pokaż wskazówkę
  • Listę dni zbuduj: val dni = (1..31).map { it.toString() }. Rok przestępny: podzielny przez 4 i niepodzielny przez 100, albo podzielny przez 400.
ZAD 6Listy zależne★★★

Pierwszy spinner: województwo (5 do wyboru). Drugi spinner: miasta z wybranego województwa (po 3–4). Zmiana województwa podmienia zawartość drugiej listy i ustawia ją na pierwszą pozycję. Pod spodem: „Lębork, woj. pomorskie — 35 km do Gdyni” (odległość do Gdyni zapisz w danych). Przycisk Losuj wybiera losowe województwo i losowe miasto.

pokaż wskazówkę
  • Miasta: tablica tablic arrayOf(arrayOf("Gdańsk", "Lębork", …), …). W słuchaczu pierwszej listy przypisz drugiej nowy ArrayAdapter z miastami. Przy losowaniu najpierw ustaw województwo, a miasto ustaw dopiero, gdy druga lista ma już nowy adapter (najprościej losować miasto w tym samym kodzie, który podmienia adapter).
PODSUMOWANIE

Co trzeba zapamiętać

  • Stałą listę wpisz do strings.xml i podaj przez android:entries.
  • Listę z kodu podłączasz przez ArrayAdapter z układami android.R.layout.simple_spinner_item i …_dropdown_item.
  • Wybór: selectedItemPosition (numer od 0) i selectedItem (tekst); ustawienie: setSelection.
  • Reakcja: onItemSelectedListener = object : AdapterView.OnItemSelectedListener { … } — wywołuje się też przy starcie.
  • Wartości związane z pozycjami trzymaj w równoległych tablicach tej samej długości.

Dokumentacja: Spinner, ArrayAdapter.

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