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

school-it.pl // android + kotlin // rozdział 7 // lekcja 28

Własny adapter i własny wiersz

Wbudowane układy wierszy pokazują jeden napis. Prawdziwe aplikacje mają w wierszu kilka rzeczy naraz: tytuł, opis, ocenę, kolorową etykietę. Do tego potrzebne są trzy elementy: klasa danych (co pokazujemy), własny układ wiersza (jak wygląda jeden wiersz) i własny adapter (kto wkłada dane do wiersza). Zbudujesz repertuar kina, w którym każdy film ma etykietę wieku, opis i gwiazdki.

Kotlin XML data class ArrayAdapter<Film> getView LayoutInflater 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Opiszesz jeden element listy klasą danych (data class).
  • Zaprojektujesz własny układ wiersza w osobnym pliku XML.
  • Napiszesz klasę adaptera dziedziczącą po ArrayAdapter i metodę getView.
  • Zrozumiesz, skąd bierze się convertView i dlaczego warto go używać.
  • Odczytasz cały obiekt po kliknięciu wiersza.
TEORIA

Trzy elementy własnej listy

Przypomnij sobie lekcję 25: ArrayAdapter brał napis z tablicy i wkładał go do gotowego wiersza simple_list_item_1. Teraz każdą z tych części robimy sami:

Film.kt
klasa danych
„co pokazujemy”
tytuł, gatunek, ocena…
+
wiersz_film.xml
układ jednego wiersza
„jak to wygląda”
→
FilmAdapter.kt
wkłada dane filmu
do kontrolek wiersza
→
ListView
wyświetla
gotowe wiersze
Lekcja 25 (wbudowany)Ta lekcja (własny)
daneArray<String>List<Film>
układ wierszaandroid.R.layout.simple_list_item_1R.layout.wiersz_film
adapterArrayAdapter(this, …, tablica)FilmAdapter(this, filmy)
kto wypełnia wierszadapter wywołuje toString()nasza metoda getView
TEORIA

Klasa danych

Jeden film to kilka informacji. Zamiast trzymać je w kilku tablicach (jak ceny w lekcji 27), zamykamy je w jednej klasie. Słowo data sprawia, że Kotlin sam dopisze przydatne metody, np. czytelne toString().

Film.kt — nowy plik w pakiecie aplikacji
package pl.schoolit.repertuar

data class Film(
    val tytul: String,
    val gatunek: String,
    val czas: Int,          // w minutach
    val ocena: Float,       // 0.0 – 5.0, dla RatingBar
    val wiek: Int           // 0 = bez ograniczeń
)

Nowy plik: prawy klik na pakiet w java/… → New → Kotlin Class/File → nazwa Film, typ Data Class.

TEORIA

Układ wiersza

Układ wiersza to zwykły plik w res/layout — taki jak activity_main.xml, tylko opisuje jeden wiersz listy. Najważniejsza różnica: główny element ma wysokość wrap_content, a nie match_parent — inaczej jeden wiersz zająłby cały ekran.

12Smocze jajoFantasy · 1 h 50 min★★★★★
wiersz_film.xml: etykieta, dwa napisy, gwiazdki

Nowy plik: prawy klik na res/layout → New → Layout Resource File → nazwa wiersz_film, główny element LinearLayout.

TEORIA

Klasa adaptera

Nasz adapter dziedziczy po ArrayAdapter<Film> — dostaje od niego za darmo liczenie wierszy i getItem. Sami piszemy tylko jedną metodę: getView, czyli „zbuduj wiersz numer position”.

szkielet własnego adaptera
class FilmAdapter(context: Context, private val filmy: List<Film>) :
    ArrayAdapter<Film>(context, 0, filmy) {      // 0 — wbudowanego układu nie używamy

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        // 1. wiersz: stary do ponownego użycia albo nowy z pliku XML
        val wiersz = convertView
            ?: LayoutInflater.from(context).inflate(R.layout.wiersz_film, parent, false)

        // 2. dane tego wiersza
        val film = filmy[position]

        // 3. dane → kontrolki wiersza
        wiersz.findViewById<TextView>(R.id.tvTytul).text = film.tytul

        // 4. gotowy wiersz oddajemy liście
        return wiersz
    }
}
FragmentZnaczenie
context: ContextAktywność, która tworzy adapter. Potrzebna do wczytania pliku XML. W MainActivity przekazujesz this.
private val filmyLista filmów jako pole adaptera — dostępna w getView.
: ArrayAdapter<Film>(context, 0, filmy)Dziedziczenie i wywołanie konstruktora rodzica.
override fun getViewPodmieniamy metodę rodzica — wiersz budujemy po swojemu.
LayoutInflater.from(context).inflate(…)„Nadmuchuje” plik XML: zamienia jego opis na prawdziwe kontrolki.
parent, falseLista jest rodzicem (dla rozmiarów), ale wiersza nie dodajemy do niej sami — zrobi to ListView.
wiersz.findViewByIdSzukamy kontrolki w wierszu, nie w całym ekranie.
TEORIA

Jak działa getView i skąd convertView

ListView nie tworzy wierszy dla wszystkich elementów. Na ekranie mieści się ich np. 7, więc powstaje około 7 wierszy. Gdy przewijasz, wiersz, który znika u góry, wraca na dół jako convertView — z nowymi danymi. To jak tablica ogłoszeń z kilkoma ramkami: nie kupujesz nowej ramki na każde ogłoszenie, tylko wymieniasz kartkę.

ListView
„potrzebuję wiersza nr 8”
→
getView(8, …)
convertView = null → inflate
convertView ≠ null → użyj go
→
wpisz dane filmu nr 8
tytuł, opis, ocena, etykieta
→
return wiersz
lista go pokazuje

Z odzyskiem wiersza wiąże się jedna zasada

Odzyskany wiersz ma jeszcze dane i wygląd poprzedniego filmu. Dlatego w getView ustawiasz każdą zmienną rzecz za każdym razem. Jeśli np. kolor ustawiasz tylko dla filmów 18+, a dla pozostałych nie — po przewinięciu czerwone etykiety pojawią się przy bajkach.

PRZYKŁAD

Repertuar kina

Lista sześciu filmów. Wiersz ma kolorowe kółko z kategorią wieku (zielone do 11 lat, pomarańczowe 12–15, czerwone 16+), tytuł, gatunek z czasem trwania i małe gwiazdki oceny. Kliknięcie wiersza otwiera okno ze szczegółami i przyciskiem Kup bilet.

18:05
Repertuar
Dziś w kinie: 6 filmów
BOKosmiczna przygodaAnimacja · 1 h 35 min★★★★★
7Wakacje z dziadkiemKomedia · 1 h 42 min★★★★★
12Smocze jajoFantasy · 1 h 50 min★★★★★
12Mistrzowie boiskaSportowy · 1 h 37 min★★★★★
15Ostatni pociągThriller · 1 h 58 min★★★★★
18Cień nad miastemKryminał · 2 h 11 min★★★★★
każdy wiersz z wiersz_film.xml
→klik
„Smocze jajo”
18:06
Repertuar
Dziś w kinie: 6 filmów
BOKosmiczna przygodaAnimacja · 1 h 35 min★★★★★
7Wakacje z dziadkiemKomedia · 1 h 42 min★★★★★
12Smocze jajoFantasy · 1 h 50 min★★★★★
12Mistrzowie boiskaSportowy · 1 h 37 min★★★★★
15Ostatni pociągThriller · 1 h 58 min★★★★★
18Cień nad miastemKryminał · 2 h 11 min★★★★★
Smocze jajo
Gatunek: Fantasy
Czas: 1 h 50 min
Ocena widzów: 3.0 / 5
Od lat: 12
ZamknijKup bilet
po kliknięciu — cały obiekt Film

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Repertuar, Kotlin, API 24.
  2. Dodaj klasę danych Film.kt (kod wyżej, w sekcji „Klasa danych”).
  3. Dodaj kształt res/drawable/tlo_wiek.xml i układ res/layout/wiersz_film.xml.
  4. Dodaj klasę FilmAdapter.kt (New → Kotlin Class/File → Class).
  5. Podmień activity_main.xml i MainActivity.kt.
app/src/main/
├── java/…/
│   ├── Film.ktklasa danych
│   ├── FilmAdapter.ktwłasny adapter
│   └── MainActivity.kt
└── res/
    ├── drawable/tlo_wiek.xmlkółko etykiety wieku
    └── layout/
        ├── activity_main.xml
        └── wiersz_film.xmlukład jednego wiersza
res/drawable/tlo_wiek.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<!-- kółko; kolor zmieniamy w adapterze -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#2E7D32" />
</shape>
res/layout/wiersz_film.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<!-- JEDEN wiersz listy: wysokość wrap_content -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="12dp">

    <TextView
        android:id="@+id/tvWiek"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="@drawable/tlo_wiek"
        android:gravity="center"
        android:textColor="#FFFFFF"
        android:textSize="13sp"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvTytul"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="1"
            android:textSize="17sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/tvOpis"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="13sp" />
    </LinearLayout>

    <!-- małe gwiazdki tylko do pokazywania oceny (lekcja 16) -->
    <RatingBar
        android:id="@+id/rbOcena"
        style="?android:attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:isIndicator="true"
        android:numStars="5"
        android:stepSize="0.5" />
</LinearLayout>
FilmAdapter.kt — pełny plik
package pl.schoolit.repertuar

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.RatingBar
import android.widget.TextView

class FilmAdapter(context: Context, private val filmy: List<Film>) :
    ArrayAdapter<Film>(context, 0, filmy) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        // 1. wiersz: odzyskany albo nowy
        val wiersz = convertView
            ?: LayoutInflater.from(context).inflate(R.layout.wiersz_film, parent, false)

        // 2. kontrolki wewnątrz wiersza
        val tvWiek = wiersz.findViewById<TextView>(R.id.tvWiek)
        val tvTytul = wiersz.findViewById<TextView>(R.id.tvTytul)
        val tvOpis = wiersz.findViewById<TextView>(R.id.tvOpis)
        val rbOcena = wiersz.findViewById<RatingBar>(R.id.rbOcena)

        // 3. dane tego wiersza
        val film = filmy[position]

        // 4. dane → kontrolki (KAŻDA rzecz ustawiana za każdym razem)
        tvTytul.text = film.tytul
        tvOpis.text = "${film.gatunek} · ${film.czas / 60} h ${film.czas % 60} min"
        rbOcena.rating = film.ocena

        tvWiek.text = if (film.wiek == 0) "BO" else film.wiek.toString()
        val kolor = when {
            film.wiek < 12 -> "#2E7D32"        // zielony
            film.wiek < 16 -> "#EF6C00"        // pomarańczowy
            else -> "#C62828"                  // czerwony
        }
        val kolko = tvWiek.background.mutate() as GradientDrawable   // lekcja 17
        kolko.setColor(Color.parseColor(kolor))

        return wiersz
    }
}
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="Repertuar"
        android:textSize="24sp"
        android:textStyle="bold" />

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

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

import android.os.Bundle
import android.widget.ListView
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 tvNaglowek: TextView
    private lateinit var lista: ListView

    // ---------- dane ----------
    private val filmy = listOf(
        Film("Kosmiczna przygoda", "Animacja", 95, 4.5f, 0),
        Film("Wakacje z dziadkiem", "Komedia", 102, 3.5f, 7),
        Film("Smocze jajo", "Fantasy", 110, 3.0f, 12),
        Film("Mistrzowie boiska", "Sportowy", 97, 4.0f, 12),
        Film("Ostatni pociąg", "Thriller", 118, 4.0f, 15),
        Film("Cień nad miastem", "Kryminał", 131, 4.5f, 18)
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUI()

        lista.adapter = FilmAdapter(this, filmy)
        tvNaglowek.text = "Dziś w kinie: ${filmy.size} filmów"

        ustawZdarzenia()
    }

    private fun initUI() {
        tvNaglowek = findViewById(R.id.tvNaglowek)
        lista = findViewById(R.id.lista)
    }

    private fun ustawZdarzenia() {
        lista.setOnItemClickListener { _, _, position, _ ->
            pokazSzczegoly(filmy[position])        // cały obiekt, nie tylko napis
        }
    }

    // ---------- akcje ----------
    private fun pokazSzczegoly(film: Film) {
        val wiek = if (film.wiek == 0) "bez ograniczeń" else "${film.wiek}"
        val tresc = "Gatunek: ${film.gatunek}\n" +
                "Czas: ${film.czas / 60} h ${film.czas % 60} min\n" +
                "Ocena widzów: ${film.ocena} / 5\n" +
                "Od lat: $wiek"

        AlertDialog.Builder(this)
            .setTitle(film.tytul)
            .setMessage(tresc)
            .setPositiveButton("Kup bilet") { _, _ ->
                Toast.makeText(this, "Bilet na „${film.tytul}” w koszyku", Toast.LENGTH_SHORT).show()
            }
            .setNegativeButton("Zamknij", null)
            .show()
    }
}

Co dzieje się po kolei

  • filmy to lista obiektów Film. Kolejność na liście = kolejność wierszy.
  • FilmAdapter(this, filmy) — this to kontekst (aktywność), filmy to dane. Adapter podłączamy jak w lekcji 25.
  • Lista pyta adapter o kolejne wiersze. getView bierze stary wiersz albo „nadmuchuje” nowy z wiersz_film.xml, znajduje w nim kontrolki i wpisuje dane filmu.
  • Czas 110 min zamienia się na „1 h 50 min”: 110 / 60 = 1 (dzielenie liczb całkowitych), 110 % 60 = 50 (reszta).
  • Kolor kółka ustawiany jest w każdym wierszu — także zielony. Dzięki temu odzyskany wiersz nie zachowa koloru poprzedniego filmu.
  • Po kliknięciu filmy[position] daje cały obiekt — w oknie są wszystkie pola, nie tylko tytuł.

Sprawdź trzy rzeczy

  1. Zmień w wiersz_film.xml wysokość głównego LinearLayout na match_parent. Każdy film zajmie cały ekran. Przywróć wrap_content.
  2. Dopisz do listy 15 kolejnych filmów (możesz kopiować). Dodaj w getView linię if (convertView == null) Log.d("ADAPTER", "nowy wiersz $position") (import android.util.Log) i przewijaj, patrząc w Logcat. Nowych wierszy będzie tylko kilkanaście — reszta to odzysk.
  3. Ustaw kolor tylko w if (film.wiek >= 16) i usuń pozostałe gałęzie. Przy wielu filmach przewiń w dół i w górę — czerwone kółka „przeskoczą” na inne filmy.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
data class Film(val …)klasaJeden element listy z kilkoma polami.
class X(…) : ArrayAdapter<Film>(context, 0, lista)klasaWłasny adapter oparty na ArrayAdapter.
override fun getView(position, convertView, parent): ViewmetodaBuduje i zwraca wiersz o podanym numerze.
convertViewView?Wiersz do ponownego użycia albo null.
LayoutInflater.from(context).inflate(R.layout.x, parent, false)metodaTworzy kontrolki z pliku XML.
?:operator„Jeśli po lewej jest null, weź to, co po prawej”.
wiersz.findViewById<T>(id)metodaSzuka kontrolki wewnątrz wiersza.
context w adapterzeContextKontekst przekazany w konstruktorze — dostępny jako właściwość adaptera.
android:maxLines="1" + android:ellipsize="end"atrybutyZa długi tytuł kończy się „…” zamiast łamać wiersz.
style="?android:attr/ratingBarStyleSmall"stylMałe gwiazdki tylko do odczytu.
a / b, a % b na IntoperatoryDzielenie całkowite i reszta z dzielenia.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
każdy wiersz na cały ekranW wiersz_film.xml główny element ma layout_height="match_parent".
inflate(R.layout.wiersz_film, parent, true) → awariaOstatni argument musi być false: wiersz do listy dodaje ListView, nie ty.
findViewById(R.id.tvTytul) bez wiersz. → błąd kompilacjiAdapter nie jest aktywnością. Szukaj w wierszu: wiersz.findViewById.
kolory lub ikony „przeskakują” przy przewijaniuJakąś rzecz ustawiasz tylko w części przypadków (if bez else). Odzyskany wiersz pamięta stary wygląd.
każdy wiersz pokazuje coś w rodzaju „Film(tytul=…, gatunek=…)”Użyto zwykłego ArrayAdapter(this, simple_list_item_1, filmy) — wypisuje toString(). Podłącz FilmAdapter.
rbOcena.rating = film.ocena nie pasuje typemrating to Float. W klasie danych Float, w liście 4.5f.
zawsze tworzysz nowy wiersz (bez convertView)Działa, ale przy długiej liście przewijanie zacina się. Zawsze convertView ?: inflate(…).
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w układzie z lekcji 19: klasa danych, układ wiersza i adapter w osobnych plikach. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Kontakty★☆☆

Klasa Kontakt(imie, nazwisko, telefon). Wiersz: pogrubione „Imię Nazwisko”, pod spodem numer telefonu szarym, mniejszym tekstem. Kliknięcie — Toast z numerem.

pokaż wskazówkę
  • Wiersz: pionowy LinearLayout z dwoma TextView; w getView: "${k.imie} ${k.nazwisko}".
ZAD 2Oceny z przedmiotów★☆☆

Klasa Ocena(przedmiot, ocena: Int). Wiersz: nazwa przedmiotu po lewej, duża cyfra oceny po prawej. Ocena 1 — czerwona, 2–3 — pomarańczowa, 4–6 — zielona. Pod listą średnia z dwoma miejscami po przecinku.

pokaż wskazówkę
  • tvOcena.setTextColor(Color.parseColor(kolor)) w każdej gałęzi when. Średnia: oceny.map { it.ocena }.average() i "%.2f".format(…).
ZAD 3Rozkład jazdy★★☆

Klasa Odjazd(godzina, linia, kierunek, opoznienie: Int). Wiersz: godzina (duża), numer linii w zaokrąglonym prostokącie (kształt z lekcji 17), kierunek. Gdy opoznienie > 0 — czerwony dopisek „+5 min”, w przeciwnym razie dopisek ukryty.

pokaż wskazówkę
  • tvOpoznienie.visibility = if (o.opoznienie > 0) View.VISIBLE else View.GONE — ustawiaj w obu przypadkach.
ZAD 4Repertuar z sortowaniem★★☆

Rozbuduj przykład: nad listą Spinner „Sortuj: tytuł / ocena / czas”. Zmiana wyboru układa filmy i odświeża listę. Lista filmy musi być teraz mutableListOf.

pokaż wskazówkę
  • filmy.sortByDescending { it.ocena } itp., potem adapter.notifyDataSetChanged() — adapter trzymaj w polu (lekcja 26).
ZAD 5Lista zadań z priorytetem★★☆

Klasa Zadanie(tytul, priorytet: Int, var zrobione: Boolean). Wiersz: pasek z lewej (szerokość 6dp) w kolorze priorytetu i tytuł. Kliknięcie przełącza zrobione: tytuł staje się szary i przekreślony.

pokaż wskazówkę
  • Przekreślenie: tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG, zdjęcie: and Paint.STRIKE_THRU_TEXT_FLAG.inv(). Ustawiaj zawsze, w obu stanach.
ZAD 6Filtr wieku w kinie★★★

Rozbuduj przykład: nad listą SeekBar „Mam lat: 13” (zakres 3–20). Lista pokazuje tylko filmy, na które widz może wejść, a nagłówek — „Dla Ciebie: 4 z 6 filmów”. Kliknięcie wiersza ma otwierać właściwy film także po filtrowaniu.

pokaż wskazówkę
  • Dwie listy: wszystkie (stała) i widoczne (adapter). Filtrowanie: widoczne.clear(); widoczne.addAll(wszystkie.filter { it.wiek <= lata }). Po kliknięciu bierz widoczne[position], nie wszystkie[position].
PODSUMOWANIE

Co trzeba zapamiętać

  • Własna lista = klasa danych + układ wiersza (wrap_content) + klasa adaptera.
  • Adapter dziedziczy po ArrayAdapter<T> i nadpisuje getView.
  • convertView ?: LayoutInflater.from(context).inflate(…, parent, false) — odzysk albo nowy wiersz.
  • Kontrolki szukasz w wierszu: wiersz.findViewById.
  • W getView ustawiaj każdą zmienną rzecz za każdym razem — wiersze wracają z wyglądem poprzedniego elementu.

W kolejnej lekcji wiersz dostanie obrazek i przyciski, a adapter — klasę ViewHolder, która oszczędza szukania kontrolek.

Dokumentacja: ArrayAdapter, LayoutInflater, Klasy danych w Kotlinie.

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