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

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.

Kotlin XML ListView ArrayAdapter simple_list_item_1 setOnItemClickListener 2 × 45 min
CEL LEKCJI

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

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.

Dane
arrayOf(„Robotyka”,
„Szachy”, …)
→
ArrayAdapter
wkłada każdy napis
do gotowego wiersza
→
ListView
wyświetla wiersze
i je przewija

W kodzie to dwie linie. Pierwsza tworzy adapter, druga podłącza go do listy:

MainActivity.kt — fragment
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, kola)
lista.adapter = adapter

Konstruktor ArrayAdapter dostaje trzy rzeczy:

ParametrCo oznacza
thisKontekst — aktywność, w której jest lista. Adapter potrzebuje go, żeby tworzyć wiersze.
android.R.layout.simple_list_item_1Wygląd jednego wiersza. Tu: wbudowany układ z jednym napisem.
kolaDane — tablica (Array) albo lista (List) napisów.
TEORIA

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

12:30
Robotyka
Szachy
Fotografia
simple_list_item_1
sam tekst — ta lekcja
12:30
Robotyka
Szachy
Fotografia
simple_list_item_single_choice
jeden wybór — lekcja 27
12:30
Robotyka
Szachy
Fotografia
simple_list_item_multiple_choice
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.

TEORIA

Kliknięcie elementu

Przycisk ma setOnClickListener. Lista ma podobną metodę dla wierszy — setOnItemClickListener. Kod w klamrach wykona się po dotknięciu dowolnego wiersza:

MainActivity.kt — fragment
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 _.

NrNazwaCo zawiera
1parentLista, w której kliknięto.
2viewKliknięty wiersz (cały widok).
3positionNumer wiersza, liczony od 0 — tak samo jak indeks w tablicy.
4idIdentyfikator 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.

TEORIA

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:

res/values/strings.xml — fragment
<string-array name="kola">
    <item>Robotyka</item>
    <item>Szachy</item>
    <item>Fotografia</item>
</string-array>
MainActivity.kt — fragment
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.

PRZYKŁAD

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.

12:30
Koła zainteresowań
Dotknij koła, aby je wybrać
Robotyka
Programowanie gier
Grafika komputerowa
Sieci komputerowe
Fotografia
Szachy
po uruchomieniu
→dotknięcie
„Programowanie gier”
12:31
Koła zainteresowań
Wybrano: Programowanie gier
Robotyka
Programowanie gier
Grafika komputerowa
Sieci komputerowe
Fotografia
Szachy
Kliknięto wiersz nr 1
po dotknięciu drugiego wiersza

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

  1. Nowy projekt: Empty Views Activity, nazwa KolaZainteresowan, język Kotlin, minimalne SDK API 24.
  2. Podmień całą zawartość activity_main.xml i MainActivity.kt na kod poniżej. Pierwszą linię package … w MainActivity.kt zostaw taką, jaką utworzył twój projekt.
app/src/main/
├── 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.

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="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>
MainActivity.kt — pełny plik
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 tvWybor i lista są zadeklarowane w klasie jako lateinit var, a wartość dostają w initUI() — tak jak od lekcji 19. Dzięki temu listę widać w każdej metodzie klasy, nie tylko w onCreate.
  • Tablica kola to 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.
  • setOnItemClickListener dostaje numer klikniętego wiersza w position. Napis pobieramy z tej samej tablicy, z której zbudowano adapter.
  • Toast pokazuje numer wiersza — dzięki temu widać, że liczenie zaczyna się od zera.

Sprawdź trzy rzeczy

  1. Kliknij pierwsze koło. Komunikat pokaże „Kliknięto wiersz nr 0”.
  2. Przewiń listę na sam dół i kliknij „Koszykówka”. Numer to 7 — choć na ekranie ten wiersz był może trzeci od góry. position to numer w danych, a nie miejsce na ekranie.
  3. Dodaj pod listą przycisk, a w ListView zmień wysokość na wrap_content i usuń wagę. Długa lista zajmie cały ekran i zepchnie przycisk poza niego. Wróć do 0dp i wagi 1 — przycisk wróci na dół ekranu, a lista zajmie miejsce nad nim.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<ListView>kontrolkaPrzewijana lista wierszy.
ArrayAdapter(kontekst, układ, dane)klasaTworzy wiersze z tablicy lub listy napisów.
android.R.layout.simple_list_item_1zasób systemuWbudowany wiersz z jednym napisem.
lista.adapter = …właściwośćPodłącza adapter do listy.
setOnItemClickListener { _, _, position, _ -> }zdarzenieKliknięcie wiersza; position to numer od 0.
arrayOf(…)funkcjaTworzy tablicę z podanych wartości.
<string-array>zasóbTablica napisów w strings.xml.
resources.getStringArray(R.array.…)Array<String>Odczytuje tablicę napisów z zasobów.
lateinit varsłowo kluczowePole, które dostanie wartość później, ale przed pierwszym użyciem.
layout_height="0dp" + layout_weight="1"atrybutyKontrolka zajmuje całe wolne miejsce w LinearLayout.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
R.layout.simple_list_item_1Unresolved reference. Układ jest systemowy — brakuje android. na początku.
lista pusta, brak błędówZapomniane 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 klasyAplikacja zamyka się przy starcie (NullPointerException). Zasoby odczytuj w onCreate.
lista.setOnClickListener { … }Nie reaguje na wiersze. Dla wierszy jest setOnItemClickListener.
ListView wewnątrz ScrollViewWidać 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

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Dni tygodnia★☆☆

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 position równe 5 albo 6 — warunek if (position >= 5).
ZAD 2Przedmioty z zasobów★☆☆

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.
ZAD 3Menu restauracji★★☆

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 position jest indeksem w obu tablicach. Kwotę z dwoma miejscami po przecinku da String.format("%.2f", suma).
ZAD 4Lista z liczbami★★☆

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ętli for (i in 1..10), a potem utwórz nowy adapter z tą listą i przypisz go do lista.adapter.
ZAD 5Quiz stolic★★☆

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().
ZAD 6Wyszukiwarka filmów★★★

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 filtrowaniu position dotyczy listy przefiltrowanej — pobieraj tytuł z niej, a nie z pełnej tablicy. Najprościej trzymać aktualnie pokazaną listę w polu klasy.
PODSUMOWANIE

Co trzeba zapamiętać

  • ListView nie 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; position liczy od 0 i pasuje do indeksu w tablicy.
  • Napisy listy mogą być w strings.xml jako string-array — odczytuj je w onCreate.

Dokumentacja: ListView, ArrayAdapter, String array w zasobach.

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