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

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

Własny adapter: obrazek i przycisk w wierszu

Wiersz z lekcji 28 tylko pokazywał dane. Teraz dostanie ikonę i przyciski − / +, które zmieniają dane bez wychodzenia z listy. Pojawią się trzy nowe rzeczy: klasa ViewHolder (wiersz pamięta swoje kontrolki), obsługa kliknięć wewnątrz wiersza i przekazanie do adaptera funkcji, którą ma wywołać po zmianie. Zbudujesz koszyk szkolnego sklepiku z sumą do zapłaty.

Kotlin XML ViewHolder view.tag ImageButton w wierszu () -> Unit 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Pokażesz w każdym wierszu inny obrazek z res/drawable.
  • Napiszesz klasę ViewHolder i zapiszesz ją w tag wiersza.
  • Obsłużysz przyciski umieszczone w wierszu listy.
  • Przekażesz do adaptera funkcję, która odświeży resztę ekranu.
  • Sprawisz, że kliknięcie wiersza i przyciski w nim działają jednocześnie.
TEORIA

Obrazek w wierszu

Obrazek z res/drawable ma w kodzie numer: R.drawable.ic_kanapka to zwykła liczba Int. Taki numer można więc zapisać w klasie danych — każdy produkt zna swoją ikonę:

numer obrazka jako pole klasy danych
data class Produkt(
    val nazwa: String,
    val cena: Double,
    val ikona: Int,          // np. R.drawable.ic_kanapka
    var ilosc: Int = 0       // var — zmienia się po kliknięciu + / −
)

// w adapterze:
ivIkona.setImageResource(produkt.ikona)       // lekcja 07

Zwróć uwagę na var ilosc: pozostałe pola są stałe, a ilość w koszyku się zmienia. Wartość = 0 jest domyślna — przy tworzeniu produktu nie trzeba jej podawać.

TEORIA

ViewHolder — wiersz pamięta swoje kontrolki

W lekcji 28 getView za każdym razem szukał kontrolek przez findViewById. Przy sześciu kontrolkach w wierszu i szybkim przewijaniu to dużo niepotrzebnego szukania — przecież odzyskany wiersz ma te same kontrolki co poprzednio. ViewHolder to mała klasa, która je zapamiętuje:

ViewHolder — szukanie tylko raz na wiersz
// „pudełko” na kontrolki jednego wiersza
private class ViewHolder(wiersz: View) {
    val ivIkona: ImageView = wiersz.findViewById(R.id.ivIkona)
    val tvNazwa: TextView = wiersz.findViewById(R.id.tvNazwa)
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val wiersz: View
    val holder: ViewHolder

    if (convertView == null) {
        // NOWY wiersz: nadmuchaj XML, znajdź kontrolki RAZ, schowaj pudełko w wierszu
        wiersz = LayoutInflater.from(context).inflate(R.layout.wiersz_produkt, parent, false)
        holder = ViewHolder(wiersz)
        wiersz.tag = holder
    } else {
        // ODZYSKANY wiersz: wyjmij gotowe pudełko
        wiersz = convertView
        holder = convertView.tag as ViewHolder
    }

    holder.tvNazwa.text = produkty[position].nazwa     // bez findViewById
    return wiersz
}
convertView == null
inflate → ViewHolder(wiersz)
→ wiersz.tag = holder
convertView ≠ null
holder = convertView.tag
as ViewHolder

tag to „kieszeń” każdej kontrolki: można w niej schować dowolny obiekt i wyjąć go później. as ViewHolder mówi Kotlinowi, co w tej kieszeni leży.

Skąd ta nazwa

View holder = „przechowywacz widoków”. To wzorzec tak popularny, że w nowszej liście RecyclerView jest obowiązkowy. Jeśli kiedyś przejdziesz na RecyclerView, ta lekcja będzie dobrym wstępem.

TEORIA

Przycisk w wierszu

Przycisk w wierszu obsługujesz w getView, tak jak każdy przycisk — setOnClickListener. Ponieważ wiersze są odzyskiwane, obsługę ustawiasz za każdym razem od nowa: nowa obsługa zastępuje starą i działa na obecnym produkcie.

przyciski w getView
val produkt = produkty[position]

holder.btnPlus.setOnClickListener {
    produkt.ilosc++                 // zmieniamy DANE…
    notifyDataSetChanged()          // …i odświeżamy listę (adapter woła to na sobie)
}

Nie zmieniaj samego napisu w wierszu (holder.tvIlosc.text = …) bez zmiany danych. Po przewinięciu wiersz zostałby narysowany od nowa z danych — i zmiana by zniknęła. Zasada z lekcji 26 obowiązuje dalej: zmieniasz dane, potem odświeżasz.

TEORIA

Adapter powiadamia aktywność

Kliknięcie + obsługuje adapter. Ale suma do zapłaty jest w MainActivity — adapter nie ma do niej dostępu. Rozwiązanie: aktywność przekazuje adapterowi funkcję „wywołaj mnie po każdej zmianie”.

funkcja jako parametr
// adapter: trzeci parametr to funkcja bez argumentów, która nic nie zwraca
class ProduktAdapter(
    context: Context,
    private val produkty: List<Produkt>,
    private val poZmianie: () -> Unit
) : ArrayAdapter<Produkt>(context, 0, produkty) { … }

// w adapterze, po kliknięciu:
poZmianie()                          // „aktywności, coś się zmieniło”

// w MainActivity — ostatni parametr-funkcję piszemy w klamrach za nawiasem:
adapter = ProduktAdapter(this, produkty) { pokazSume() }
klik +
w adapterze
→
produkt.ilosc++
notifyDataSetChanged()
→
poZmianie()
funkcja od aktywności
→
pokazSume()
w MainActivity

() -> Unit czytaj: „funkcja, która nie przyjmuje nic i nic nie zwraca”. Znasz to już z setOnClickListener { … } — tam też przekazujesz kawałek kodu do wykonania później.

TEORIA

Przyciski a kliknięcie całego wiersza

Gdy w wierszu jest przycisk, ListView często przestaje reagować na kliknięcie samego wiersza: przycisk „przejmuje fokus” całego wiersza. Naprawia to jeden atrybut na głównym elemencie wiersz_produkt.xml:

główny element wiersza
<LinearLayout …
    android:descendantFocusability="blocksDescendants">   <!-- dzieci nie zabierają fokusu wierszowi -->

Przyciski nadal reagują na dotyk, a kliknięcie w resztę wiersza trafia do setOnItemClickListener.

PRZYKŁAD

Sklepik szkolny

Lista sześciu produktów z ikoną, nazwą i ceną. Przyciski − / + zmieniają ilość; wiersz z produktem w koszyku ma jasnofioletowe tło, a − przy zerze jest przygaszony. Kliknięcie wiersza pokazuje Toast z wartością tej pozycji. Pod listą — liczba sztuk i suma. Zamów pokazuje okno z podsumowaniem; po potwierdzeniu koszyk się zeruje.

10:40
Sklepik szkolny
🥪Kanapka z serem4,50 zł−2+
💧Woda 0,5 l2,00 zł−0+
🥐Drożdżówka3,00 zł−0+
🍪Ciastko1,50 zł−1+
☕Kakao3,50 zł−0+
🍦Lody4,00 zł−0+
Sztuk: 3 · Do zapłaty: 10,50 zł
Wyczyść
Zamów
2 × kanapka, 1 × ciastko
→Zamów
10:40
Sklepik szkolny
🥪Kanapka z serem4,50 zł−2+
💧Woda 0,5 l2,00 zł−0+
🥐Drożdżówka3,00 zł−0+
🍪Ciastko1,50 zł−1+
☕Kakao3,50 zł−0+
🍦Lody4,00 zł−0+
Sztuk: 3 · Do zapłaty: 10,50 zł
Wyczyść
Zamów
Twoje zamówienie
2 × Kanapka z serem
1 × Ciastko

Razem: 10,50 zł
WróćZamawiam
okno z listą pozycji
10:41
Sklepik szkolny
🥪Kanapka z serem4,50 zł−0+
💧Woda 0,5 l2,00 zł−0+
🥐Drożdżówka3,00 zł−0+
🍪Ciastko1,50 zł−0+
☕Kakao3,50 zł−0+
🍦Lody4,00 zł−0+
Koszyk jest pusty
Wyczyść
Zamów
Zamówienie przyjęte — odbiór na przerwie
po zamówieniu — koszyk wyzerowany

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Sklepik, Kotlin, API 24.
  2. Dodaj ikony: prawy klik na res → New → Vector Asset → Clip art. Nazwy plików i obrazki: ic_kanapka (lunch dining), ic_woda (water drop), ic_drozdzowka (bakery dining), ic_ciastko (cookie), ic_kakao (local cafe), ic_lody (icecream), ic_plus (add), ic_minus (remove).
  3. Dodaj Produkt.kt, wiersz_produkt.xml i ProduktAdapter.kt.
  4. Podmień activity_main.xml i MainActivity.kt.
app/src/main/
├── java/…/
│   ├── Produkt.ktklasa danych z var ilosc
│   ├── ProduktAdapter.ktadapter z ViewHolder i przyciskami
│   └── MainActivity.kt
└── res/
    ├── drawable/ic_kanapka.xml … ic_plus.xml, ic_minus.xml8 ikon
    └── layout/
        ├── activity_main.xml
        └── wiersz_produkt.xml
Produkt.kt — pełny plik
package pl.schoolit.sklepik

data class Produkt(
    val nazwa: String,
    val cena: Double,
    val ikona: Int,          // R.drawable.…
    var ilosc: Int = 0       // ile sztuk w koszyku
)
res/layout/wiersz_produkt.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="8dp">

    <ImageView
        android:id="@+id/ivIkona"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:contentDescription="Ikona produktu" />

    <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/tvNazwa"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="1"
            android:textSize="16sp"
            android:textStyle="bold" />

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

    <ImageButton
        android:id="@+id/btnMinus"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="?attr/selectableItemBackgroundBorderless"
        android:contentDescription="Odejmij"
        android:src="@drawable/ic_minus" />

    <TextView
        android:id="@+id/tvIlosc"
        android:layout_width="32dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="16sp"
        android:textStyle="bold" />

    <ImageButton
        android:id="@+id/btnPlus"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="?attr/selectableItemBackgroundBorderless"
        android:contentDescription="Dodaj"
        android:src="@drawable/ic_plus" />
</LinearLayout>
ProduktAdapter.kt — pełny plik
package pl.schoolit.sklepik

import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView

class ProduktAdapter(
    context: Context,
    private val produkty: List<Produkt>,
    private val poZmianie: () -> Unit          // wywołamy po każdym + / −
) : ArrayAdapter<Produkt>(context, 0, produkty) {

    // ---------- pudełko na kontrolki jednego wiersza ----------
    private class ViewHolder(wiersz: View) {
        val ivIkona: ImageView = wiersz.findViewById(R.id.ivIkona)
        val tvNazwa: TextView = wiersz.findViewById(R.id.tvNazwa)
        val tvCena: TextView = wiersz.findViewById(R.id.tvCena)
        val tvIlosc: TextView = wiersz.findViewById(R.id.tvIlosc)
        val btnMinus: ImageButton = wiersz.findViewById(R.id.btnMinus)
        val btnPlus: ImageButton = wiersz.findViewById(R.id.btnPlus)
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        // 1. wiersz i jego pudełko
        val wiersz: View
        val holder: ViewHolder
        if (convertView == null) {
            wiersz = LayoutInflater.from(context).inflate(R.layout.wiersz_produkt, parent, false)
            holder = ViewHolder(wiersz)
            wiersz.tag = holder
        } else {
            wiersz = convertView
            holder = convertView.tag as ViewHolder
        }

        // 2. dane → kontrolki (każda rzecz za każdym razem)
        val produkt = produkty[position]
        val wKoszyku = produkt.ilosc > 0

        holder.ivIkona.setImageResource(produkt.ikona)
        holder.tvNazwa.text = produkt.nazwa
        holder.tvCena.text = "%.2f zł".format(produkt.cena)
        holder.tvIlosc.text = produkt.ilosc.toString()
        holder.btnMinus.isEnabled = wKoszyku
        holder.btnMinus.alpha = if (wKoszyku) 1f else 0.3f
        wiersz.setBackgroundColor(if (wKoszyku) Color.parseColor("#EADDFF") else Color.TRANSPARENT)

        // 3. przyciski w wierszu — zmieniają DANE tego produktu
        holder.btnPlus.setOnClickListener {
            produkt.ilosc++
            notifyDataSetChanged()
            poZmianie()
        }
        holder.btnMinus.setOnClickListener {
            if (produkt.ilosc > 0) produkt.ilosc--
            notifyDataSetChanged()
            poZmianie()
        }

        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="Sklepik szkolny"
        android:textSize="24sp"
        android:textStyle="bold" />

    <ListView
        android:id="@+id/lista"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <TextView
        android:id="@+id/tvSuma"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textSize="16sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/btnWyczysc"
            style="@style/Widget.Material3.Button.OutlinedButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:text="Wyczyść" />

        <Button
            android:id="@+id/btnZamow"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Zamów" />
    </LinearLayout>

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

import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar

class MainActivity : AppCompatActivity() {

    // ---------- kontrolki ----------
    private lateinit var glownyUklad: View
    private lateinit var lista: ListView
    private lateinit var tvSuma: TextView
    private lateinit var btnWyczysc: Button
    private lateinit var btnZamow: Button

    // ---------- dane ----------
    private val produkty = listOf(
        Produkt("Kanapka z serem", 4.50, R.drawable.ic_kanapka),
        Produkt("Woda 0,5 l", 2.00, R.drawable.ic_woda),
        Produkt("Drożdżówka", 3.00, R.drawable.ic_drozdzowka),
        Produkt("Ciastko", 1.50, R.drawable.ic_ciastko),
        Produkt("Kakao", 3.50, R.drawable.ic_kakao),
        Produkt("Lody", 4.00, R.drawable.ic_lody)
    )
    private lateinit var adapter: ProduktAdapter

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

        // trzeci parametr — funkcja, którą adapter wywoła po każdej zmianie
        adapter = ProduktAdapter(this, produkty) { pokazSume() }
        lista.adapter = adapter

        ustawZdarzenia()
        pokazSume()
    }

    private fun initUI() {
        glownyUklad = findViewById(R.id.main)
        lista = findViewById(R.id.lista)
        tvSuma = findViewById(R.id.tvSuma)
        btnWyczysc = findViewById(R.id.btnWyczysc)
        btnZamow = findViewById(R.id.btnZamow)
    }

    private fun ustawZdarzenia() {
        // klik w wiersz (poza przyciskami) — działa dzięki blocksDescendants
        lista.setOnItemClickListener { _, _, position, _ ->
            val p = produkty[position]
            val wartosc = "%.2f".format(p.cena * p.ilosc)
            Toast.makeText(this, "${p.nazwa}: ${p.ilosc} szt. = $wartosc zł", Toast.LENGTH_SHORT).show()
        }
        btnWyczysc.setOnClickListener { wyczysc() }
        btnZamow.setOnClickListener { zapytajOZamowienie() }
    }

    // ---------- akcje ----------
    private fun wyczysc() {
        for (p in produkty) p.ilosc = 0
        adapter.notifyDataSetChanged()
        pokazSume()
    }

    private fun zapytajOZamowienie() {
        val pozycje = produkty
            .filter { it.ilosc > 0 }
            .joinToString("\n") { "${it.ilosc} × ${it.nazwa}" }

        AlertDialog.Builder(this)
            .setTitle("Twoje zamówienie")
            .setMessage("$pozycje\n\nRazem: ${"%.2f".format(suma())} zł")
            .setPositiveButton("Zamawiam") { _, _ ->
                wyczysc()
                Snackbar.make(glownyUklad, "Zamówienie przyjęte — odbiór na przerwie", Snackbar.LENGTH_LONG).show()
            }
            .setNegativeButton("Wróć", null)
            .show()
    }

    // ---------- obliczenia ----------
    private fun suma(): Double = produkty.sumOf { it.cena * it.ilosc }

    // ---------- dane → ekran ----------
    private fun pokazSume() {
        val sztuk = produkty.sumOf { it.ilosc }
        if (sztuk == 0) {
            tvSuma.text = "Koszyk jest pusty"
        } else {
            tvSuma.text = "Sztuk: $sztuk · Do zapłaty: ${"%.2f".format(suma())} zł"
        }
        btnZamow.isEnabled = sztuk > 0
        btnWyczysc.isEnabled = sztuk > 0
    }
}

Co dzieje się po kolei

  • Produkty mają ikonę jako numer zasobu (R.drawable.ic_kanapka) i ilość 0 na starcie.
  • Adapter dostaje funkcję { pokazSume() }. Sam nie wie, co ona robi — wie tylko, że ma ją wywołać po zmianie.
  • getView: nowy wiersz dostaje ViewHolder w tag; odzyskany — wyjmuje go z tag. Potem wypełnia wszystkie kontrolki, także tło i przygaszenie −.
  • + zwiększa ilosc w obiekcie produktu, odświeża listę i woła poZmianie() → pokazSume() w aktywności.
  • "%.2f".format(…) formatuje liczbę z dwoma miejscami po przecinku; polski telefon wstawi przecinek: „10,50”.
  • wyczysc() zeruje ilości w obiektach — lista produktów zostaje ta sama, zmienia się tylko jej zawartość.
  • Kliknięcie w nazwę lub ikonę trafia do setOnItemClickListener, bo wiersz ma blocksDescendants.

Sprawdź trzy rzeczy

  1. Usuń z wiersz_produkt.xml atrybut descendantFocusability. Przyciski działają, ale kliknięcie w nazwę produktu może przestać pokazywać Toast. Przywróć atrybut.
  2. W getView zakomentuj linię z setBackgroundColor. Dodaj kanapkę, wyczyść koszyk. Kolor tła nie zmieni się, bo nikt go nie ustawia — każdą zmienną rzecz ustawiaj zawsze.
  3. W obsłudze + zakomentuj poZmianie(). Ilość w wierszu rośnie, a suma pod listą stoi — aktywność nie wie o zmianie.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
private class ViewHolder(wiersz: View)klasaPamięta kontrolki jednego wiersza.
view.tag = obiekt, view.tag as TypwłaściwośćSchowanie i wyjęcie dowolnego obiektu z kontrolki.
imageView.setImageResource(R.drawable.x)metodaUstawia obrazek z zasobów.
val f: () -> Unittyp funkcjiFunkcja bez argumentów, nic nie zwraca.
Klasa(a, b) { … }składniaOstatni parametr-funkcję można podać w klamrach za nawiasem.
notifyDataSetChanged() w adapterzemetodaAdapter odświeża sam siebie po kliknięciu w wierszu.
android:descendantFocusability="blocksDescendants"atrybutPrzyciski w wierszu nie blokują kliknięcia wiersza.
?attr/selectableItemBackgroundBorderlesstłoPrzezroczyste tło przycisku z okrągłym „błyskiem” po dotknięciu.
"%.2f".format(liczba)StringLiczba z dwoma miejscami po przecinku.
sumOf { … }, filter { … }, joinToString("\n") { … }metody listSuma, wybór elementów, sklejenie w tekst.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
convertView.tag as ViewHolder → awaria NullPointerExceptionPrzy tworzeniu nowego wiersza zabrakło wiersz.tag = holder.
ilość zmienia się w złym wierszuObsługa przycisku ustawiona tylko dla nowych wierszy (w if (convertView == null)). Ustawiaj ją przy każdym getView, poza if.
po przewinięciu ilość wraca do starejZmieniono tylko napis (tvIlosc.text), a nie dane (produkt.ilosc).
val ilosc: Int i produkt.ilosc++ → błąd kompilacjiPole zmieniane po kliknięciu musi być var.
suma się nie zmieniaAdapter nie wywołuje poZmianie() albo aktywność przekazała pustą funkcję { }.
kliknięcie wiersza nie działa, przyciski takBrak descendantFocusability="blocksDescendants".
ikona niewidocznaW klasie danych zapisano 0 albo zły numer — podawaj R.drawable.nazwa.
ZADANIA

Zadania

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

ZAD 1Ulubione zwierzęta★☆☆

Klasa Zwierze(nazwa, ikona, var ulubione: Boolean). Wiersz: ikona, nazwa i ImageButton z serduszkiem (puste / pełne — dwie ikony favorite_border i favorite). Kliknięcie serduszka przełącza ulubione.

pokaż wskazówkę
  • holder.btnSerce.setImageResource(if (z.ulubione) R.drawable.ic_serce else R.drawable.ic_serce_puste) — w każdym getView.
ZAD 2Głosowanie na samorząd★☆☆

Klasa Kandydat(imie, klasa, var glosy: Int). Wiersz: imię, klasa, liczba głosów i przycisk Głosuj. Pod listą „Oddano głosów: 17”, a nad nią „Prowadzi: Ola (3B)”.

pokaż wskazówkę
  • Lider: kandydaci.maxByOrNull { it.glosy }. Napisy aktualizuj w funkcji przekazanej do adaptera.
ZAD 3Lista zakupów z ikoną usuwania★★☆

Przerób przykład z lekcji 26: wiersz ma nazwę i ImageButton z koszem (delete). Kosz usuwa produkt i pokazuje Snackbar z cofnięciem. Adapter dostaje dwie funkcje: „usuń element nr …” i „odśwież licznik”.

pokaż wskazówkę
  • Funkcja z argumentem: private val usun: (Int) -> Unit; w adapterze usun(position), w aktywności ZakupyAdapter(this, zakupy, { nr -> usunZCofnieciem(nr) }, { pokazLicznik() }).
ZAD 4Limit w sklepiku★★☆

Rozbuduj przykład: każdy produkt ma pole naStanie. Przy ilości równej stanowi + jest przygaszony, a pod nazwą pojawia się czerwony napis „ostatnie sztuki”, gdy na stanie zostały mniej niż 3. Budżet ucznia 15 zł — przycisk + nie pozwala go przekroczyć (Toast „Brak środków”).

pokaż wskazówkę
  • Budżet zna aktywność — przekaż adapterowi funkcję czyStac: (Double) -> Boolean i zapytaj ją przed ilosc++.
ZAD 5Odtwarzacz playlisty★★☆

Klasa Utwor(tytul, wykonawca, czas). Wiersz ma przycisk ▶. Kliknięcie ▶ sprawia, że ten wiersz pokazuje ⏸ i pogrubiony tytuł, a wszystkie inne — ▶ i zwykły tytuł. Nad listą „Teraz gra: …”.

pokaż wskazówkę
  • Numer grającego utworu trzymaj w adapterze: var grany = -1. W getView: val gra = position == grany i od tego ikona oraz styl tekstu.
ZAD 6Plan lekcji z edycją★★★

Klasa Lekcja(nr, przedmiot, sala) z ikoną przedmiotu. Wiersz ma przyciski ✎ i 🗑 (ikony edit, delete). ✎ otwiera AlertDialog z dwoma polami (przedmiot i sala, lekcja 24) wypełnionymi obecnymi danymi; zapis zmienia obiekt i listę. 🗑 pyta o potwierdzenie. Przycisk pod listą dodaje nową lekcję z kolejnym numerem.

pokaż wskazówkę
  • Okna otwiera aktywność, nie adapter: przekaż funkcje edytuj: (Int) -> Unit i usun: (Int) -> Unit. Pola przedmiotu i sali w klasie danych muszą być var.
PODSUMOWANIE

Co trzeba zapamiętać

  • Obrazek w wierszu: numer R.drawable.… jako pole klasy danych i setImageResource.
  • ViewHolder szuka kontrolek raz na wiersz; trzymasz go w wiersz.tag.
  • Przyciski w wierszu: obsługa w każdym getView, zmiana danych, potem notifyDataSetChanged().
  • Adapter informuje aktywność przez funkcję przekazaną w konstruktorze (() -> Unit).
  • descendantFocusability="blocksDescendants" — przyciski i kliknięcie wiersza działają razem.

To koniec rozdziału o listach. W następnym przejdziesz do wielu ekranów: intencje, przekazywanie danych i obiektów (np. kliknięty Produkt na ekran szczegółów).

Dokumentacja: View.setTag, descendantFocusability, Funkcje jako parametry w Kotlinie.

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