school-it.pl // android + kotlin // rozdział 7 // lekcja 25
ListView i ArrayAdapter z wbudowanym układem
Kontakty, wiadomości, ustawienia, lista zakupów — prawie każda aplikacja pokazuje coś w postaci listy. W tej lekcji wyświetlisz listę napisów w kontrolce ListView. Nie zaprojektujesz przy tym ani jednego wiersza: użyjesz gotowego układu, który Android ma wbudowany, i adaptera ArrayAdapter, który zamieni tablicę napisów na wiersze listy.
Czego się dziś nauczysz
- Dodasz do ekranu kontrolkę
ListView. - Połączysz tablicę napisów z listą za pomocą
ArrayAdapter. - Użyjesz wbudowanego układu wiersza
android.R.layout.simple_list_item_1. - Odczytasz, który element listy kliknął użytkownik.
- Wczytasz elementy listy z pliku
strings.xml.
Lista i adapter
ListView to kontrolka, która wyświetla elementy jeden pod drugim i sama je przewija. Sama z siebie nie wie, co ma pokazać — nie da się jej po prostu „wrzucić” tablicy, tak jak wpisuje się tekst do TextView.
Między danymi a listą stoi adapter. Pomyśl o nim jak o kelnerze: dane to kuchnia, lista to stolik. Lista mówi „potrzebuję wiersza numer 3”, a adapter bierze z tablicy element numer 3, wkłada go do gotowego wiersza i podaje liście. Lista prosi tylko o te wiersze, które akurat widać na ekranie — dlatego nawet tysiąc elementów nie spowolni telefonu.
„Szachy”, …)
do gotowego wiersza
i je przewija
W kodzie to dwie linie. Pierwsza tworzy adapter, druga podłącza go do listy:
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, kola)
lista.adapter = adapter
Konstruktor ArrayAdapter dostaje trzy rzeczy:
| Parametr | Co oznacza |
|---|---|
this | Kontekst — aktywność, w której jest lista. Adapter potrzebuje go, żeby tworzyć wiersze. |
android.R.layout.simple_list_item_1 | Wygląd jednego wiersza. Tu: wbudowany układ z jednym napisem. |
kola | Dane — tablica (Array) albo lista (List) napisów. |
Wbudowane układy wierszy
Android ma w sobie kilka gotowych plików XML z wyglądem wiersza. Odwołujemy się do nich przez android.R.layout… — słowo android. na początku oznacza „zasób systemu”, a nie twojego projektu. Twoje pliki są w R.layout…, systemowe w android.R.layout….
sam tekst — ta lekcja
jeden wybór — lekcja 27
wiele wyborów — lekcja 27
trzy wbudowane układy wierszy — zmienia się drugi parametr ArrayAdapter (listy wyboru potrzebują jeszcze atrybutu choiceMode, poznasz go w lekcji 27)
Układ simple_list_item_1 to w środku jeden TextView o wysokości co najmniej 48 dp — tyle, żeby wygodnie trafić palcem. Gdy wiersz ma wyglądać inaczej (obrazek, dwa napisy, przycisk), potrzebny będzie własny układ i własny adapter — to temat lekcji 28.
ArrayAdapter i obiekty
Gdy w tablicy są obiekty zamiast napisów, ArrayAdapter wywoła na każdym metodę toString() i to ten tekst pokaże w wierszu. Wykorzystamy to w lekcji 32.
Kliknięcie elementu
Przycisk ma setOnClickListener. Lista ma podobną metodę dla wierszy — setOnItemClickListener. Kod w klamrach wykona się po dotknięciu dowolnego wiersza:
lista.setOnItemClickListener { _, _, position, _ ->
val kolo = kola[position] // napis z klikniętego wiersza
tvWybor.text = "Wybrano: $kolo"
}
Android przekazuje do klamer cztery wartości. Potrzebna jest tylko trzecia — position, czyli numer klikniętego wiersza. Pozostałych nie używamy, więc w ich miejscu piszemy podkreślnik _.
| Nr | Nazwa | Co zawiera |
|---|---|---|
| 1 | parent | Lista, w której kliknięto. |
| 2 | view | Kliknięty wiersz (cały widok). |
| 3 | position | Numer wiersza, liczony od 0 — tak samo jak indeks w tablicy. |
| 4 | id | Identyfikator wiersza. Przy ArrayAdapter jest równy position. |
Ponieważ numeracja wierszy i indeksy tablicy zaczynają się od zera, kola[position] zawsze daje dokładnie ten napis, który był w klikniętym wierszu.
Dane z zasobów
Napisów nie trzeba trzymać w kodzie. Plik res/values/strings.xml może zawierać całą tablicę napisów — string-array:
<string-array name="kola">
<item>Robotyka</item>
<item>Szachy</item>
<item>Fotografia</item>
</string-array>
private lateinit var kola: Array<String> // pole klasy — wartość nada onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
kola = resources.getStringArray(R.array.kola)
// dalej adapter jak wcześniej
}
Zasoby dopiero w onCreate
Zapis private val kola = resources.getStringArray(R.array.kola) jako pole klasy wygląda krócej, ale aplikacja zamknie się przy starcie. Pola klasy tworzą się, zanim aktywność zostanie podłączona do systemu, a wtedy resources jeszcze nie działa. Dlatego pole jest lateinit var, a wartość dostaje w onCreate.
Koła zainteresowań
Szkoła ma osiem kół zainteresowań. Aplikacja pokazuje je na liście. Po dotknięciu koła napis nad listą zmienia się na „Wybrano: …”, a na dole pojawia się krótki komunikat z numerem wiersza.
„Programowanie gier”
drugi wiersz ma numer 1, bo numeracja zaczyna się od zera; pozostałe dwa koła są niżej — listę można przewinąć
Przygotowanie projektu
- Nowy projekt: Empty Views Activity, nazwa
KolaZainteresowan, język Kotlin, minimalne SDK API 24. - Podmień całą zawartość
activity_main.xmliMainActivity.ktna kod poniżej. Pierwszą liniępackage …wMainActivity.ktzostaw taką, jaką utworzył twój projekt.
├── java/…/MainActivity.ktkod ekranu
└── res/
└── layout/activity_main.xmlwygląd ekranu
Dlaczego w kursie nie ma enableEdgeToEdge()
Szablon Android Studio dodaje do MainActivity wywołanie enableEdgeToEdge() i kilka linii z ViewCompat, które odsuwają treść od paska stanu. W kursie robi to jeden atrybut android:fitsSystemWindows="true" w układzie (szczegóły w lekcji 01). Ponieważ ten atrybut ustawia własny padding, odstęp od krawędzi ekranu dajemy przez layout_margin.
<?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="Koła zainteresowań"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvWybor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="Dotknij koła, aby je wybrać"
android:textSize="18sp" />
<!-- wysokość 0dp + waga 1: lista zajmuje całe pozostałe miejsce -->
<ListView
android:id="@+id/lista"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
package pl.schoolit.kolazainteresowan
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// kontrolki ekranu — znajdzie je initUI()
private lateinit var tvWybor: TextView
private lateinit var lista: ListView
// dane listy
private val kola = arrayOf(
"Robotyka", "Programowanie gier", "Grafika komputerowa", "Sieci komputerowe",
"Fotografia", "Szachy", "Język angielski", "Koszykówka"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
// adapter: dane + wbudowany wygląd wiersza
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, kola)
lista.adapter = adapter
lista.setOnItemClickListener { _, _, position, _ ->
val kolo = kola[position]
tvWybor.text = "Wybrano: $kolo"
Toast.makeText(this, "Kliknięto wiersz nr $position", Toast.LENGTH_SHORT).show()
}
}
private fun initUI() {
tvWybor = findViewById(R.id.tvWybor)
lista = findViewById(R.id.lista)
}
}
Co dzieje się po kolei
- Pola
tvWyborilistasą zadeklarowane w klasie jakolateinit var, a wartość dostają winitUI()— tak jak od lekcji 19. Dzięki temu listę widać w każdej metodzie klasy, nie tylko wonCreate. - Tablica
kolato zwykłe dane w Kotlinie. Lista jeszcze nic o niej nie wie. ArrayAdapter(this, android.R.layout.simple_list_item_1, kola)tworzy adapter dla tej tablicy z wbudowanym wyglądem wiersza.lista.adapter = adapterłączy listę z adapterem — od tej chwili na ekranie widać wiersze.setOnItemClickListenerdostaje numer klikniętego wiersza wposition. Napis pobieramy z tej samej tablicy, z której zbudowano adapter.Toastpokazuje numer wiersza — dzięki temu widać, że liczenie zaczyna się od zera.
Sprawdź trzy rzeczy
- Kliknij pierwsze koło. Komunikat pokaże „Kliknięto wiersz nr 0”.
- Przewiń listę na sam dół i kliknij „Koszykówka”. Numer to 7 — choć na ekranie ten wiersz był może trzeci od góry.
positionto numer w danych, a nie miejsce na ekranie. - Dodaj pod listą przycisk, a w
ListViewzmień wysokość nawrap_contenti usuń wagę. Długa lista zajmie cały ekran i zepchnie przycisk poza niego. Wróć do0dpi wagi1— przycisk wróci na dół ekranu, a lista zajmie miejsce nad nim.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<ListView> | kontrolka | Przewijana lista wierszy. |
ArrayAdapter(kontekst, układ, dane) | klasa | Tworzy wiersze z tablicy lub listy napisów. |
android.R.layout.simple_list_item_1 | zasób systemu | Wbudowany wiersz z jednym napisem. |
lista.adapter = … | właściwość | Podłącza adapter do listy. |
setOnItemClickListener { _, _, position, _ -> } | zdarzenie | Kliknięcie wiersza; position to numer od 0. |
arrayOf(…) | funkcja | Tworzy tablicę z podanych wartości. |
<string-array> | zasób | Tablica napisów w strings.xml. |
resources.getStringArray(R.array.…) | Array<String> | Odczytuje tablicę napisów z zasobów. |
lateinit var | słowo kluczowe | Pole, które dostanie wartość później, ale przed pierwszym użyciem. |
layout_height="0dp" + layout_weight="1" | atrybuty | Kontrolka zajmuje całe wolne miejsce w LinearLayout. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
R.layout.simple_list_item_1 | Unresolved reference. Układ jest systemowy — brakuje android. na początku. |
| lista pusta, brak błędów | Zapomniane lista.adapter = adapter. Sam adapter niczego nie wyświetla. |
kola[position + 1] | Pokazuje następny element, a przy ostatnim wierszu aplikacja się zamyka (ArrayIndexOutOfBoundsException). position już liczy od 0. |
private val kola = resources.getStringArray(…) w polu klasy | Aplikacja zamyka się przy starcie (NullPointerException). Zasoby odczytuj w onCreate. |
lista.setOnClickListener { … } | Nie reaguje na wiersze. Dla wierszy jest setOnItemClickListener. |
ListView wewnątrz ScrollView | Widać tylko jeden wiersz — dwa przewijania się gryzą. Lista przewija się sama, nie wkładaj jej w ScrollView. |
użycie lista przed initUI() | UninitializedPropertyAccessException: lateinit property lista has not been initialized. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Lista siedmiu dni tygodnia. Po kliknięciu dnia napis nad listą pokazuje „Poniedziałek to 1. dzień tygodnia” (numer o jeden większy niż position). Sobota i niedziela dodatkowo pokazują Toast „Weekend!”.
pokaż wskazówkę
- Weekend to
positionrówne 5 albo 6 — warunekif (position >= 5).
Wpisz do strings.xml tablicę przedmioty z dziesięcioma przedmiotami szkolnymi i wyświetl ją na liście. W kodzie Kotlin nie może być ani jednej nazwy przedmiotu. Pod listą umieść TextView z napisem „Przedmiotów: 10” — liczbę pobierz z tablicy.
pokaż wskazówkę
- Liczbę elementów tablicy podaje
przedmioty.size.
Dwie tablice tej samej długości: dania (napisy) i ceny (liczby Double). Lista pokazuje tylko nazwy dań. Po kliknięciu dania pod listą pojawia się „Pierogi ruskie — 24,50 zł”. Każde kolejne kliknięcie dodaje cenę do rachunku wyświetlanego w drugim TextView („Do zapłaty: 61,00 zł”), a przycisk Nowy rachunek zeruje sumę.
pokaż wskazówkę
- Ten sam
positionjest indeksem w obu tablicach. Kwotę z dwoma miejscami po przecinku daString.format("%.2f", suma).
Aplikacja tworzy w pętli listę napisów „1 × 7 = 7”, „2 × 7 = 14”, … aż do „10 × 7 = 70” i wyświetla ją w ListView. Pole EditText i przycisk Pokaż pozwalają zmienić liczbę 7 na inną — lista buduje się od nowa.
pokaż wskazówkę
- Zbuduj
val wiersze = mutableListOf<String>(), dodawaj w pętlifor (i in 1..10), a potem utwórz nowy adapter z tą listą i przypisz go dolista.adapter.
Nad listą wyświetla się pytanie „Stolica Niemiec?”. Lista zawiera sześć miast. Kliknięcie poprawnego miasta pokazuje Toast „Dobrze!” i losuje następne pytanie; kliknięcie złego — Toast „Spróbuj jeszcze raz”. Pytania i odpowiedzi trzymaj w dwóch tablicach: panstwa i stolice; lista pokazuje tablicę stolice.
pokaż wskazówkę
- Pamiętaj w polu klasy numer bieżącego pytania. Odpowiedź jest dobra, gdy
position == numerPytania. Losowanie:(0 until panstwa.size).random().
Lista 15 tytułów filmów. Nad listą jest EditText „Szukaj…” i przycisk Filtruj: lista pokazuje tylko tytuły zawierające wpisany tekst (bez znaczenia wielkie i małe litery). Puste pole przywraca całą listę. Kliknięcie filmu pokazuje jego tytuł nad listą — także wtedy, gdy lista jest przefiltrowana. Gdy nic nie pasuje, pod polem pojawia się „Brak wyników”.
pokaż wskazówkę
- Filtrowanie:
filmy.filter { it.contains(szukane, ignoreCase = true) }. Po filtrowaniupositiondotyczy listy przefiltrowanej — pobieraj tytuł z niej, a nie z pełnej tablicy. Najprościej trzymać aktualnie pokazaną listę w polu klasy.
Co trzeba zapamiętać
ListViewnie przyjmuje danych bezpośrednio — potrzebuje adaptera.ArrayAdapter(this, android.R.layout.simple_list_item_1, dane)zamienia tablicę napisów na wiersze z gotowym wyglądem.- Systemowe układy to
android.R.layout…, twoje własne —R.layout…. - Kliknięcie wiersza obsługuje
setOnItemClickListener;positionliczy od 0 i pasuje do indeksu w tablicy. - Napisy listy mogą być w
strings.xmljakostring-array— odczytuj je wonCreate.
Dokumentacja: ListView, ArrayAdapter, String array w zasobach.