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

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

Dodawanie i usuwanie elementów listy

W lekcji 25 lista pokazywała stałą tablicę — nic nie dało się dopisać ani usunąć. Prawdziwe listy żyją: dopisujesz zakupy, odhaczasz kupione, kasujesz zbędne. W tej lekcji zamienisz tablicę na listę, którą można zmieniać, nauczysz się „powiadamiać” adapter o zmianach i zbudujesz listę zakupów z dodawaniem, oznaczaniem, usuwaniem (z potwierdzeniem i cofnięciem) i sortowaniem.

Kotlin XML MutableList notifyDataSetChanged setOnItemLongClickListener emptyView 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Podłączysz do ListView listę MutableList zamiast stałej tablicy.
  • Dodasz, zmienisz i usuniesz element, a potem odświeżysz listę na ekranie.
  • Obsłużysz długie przytrzymanie wiersza.
  • Połączysz listę z oknem potwierdzenia (lekcja 22) i Snackbarem z cofnięciem (lekcja 21).
  • Pokażesz komunikat, gdy lista jest pusta.
TEORIA

Lista, którą można zmieniać

Tablica (arrayOf) ma stały rozmiar — nie da się do niej dopisać elementu. Kotlin ma do tego MutableList („lista zmienna”):

lista zmienna jako pole klasy
private val zakupy = mutableListOf("Chleb", "Mleko", "Jabłka")
private lateinit var adapter: ArrayAdapter<String>

// w onCreate, po initUI():
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, zakupy)
lista.adapter = adapter

Zwróć uwagę: lista jest zadeklarowana przez val, a mimo to można ją zmieniać. val oznacza, że pole zawsze wskazuje tę samą listę — ale zawartość tej listy może się zmieniać. To ważne: adapter dostał tę konkretną listę i musi pracować z nią do końca.

Adapter też jest polem klasy — będzie potrzebny w wielu metodach (dodawanie, usuwanie, sortowanie).

TEORIA

Powiadomienie adaptera

Adapter nie pilnuje listy przez cały czas. Gdy dopiszesz element, lista w pamięci się zmieni, ale ekran — nie. Trzeba „powiadomić” adapter, że dane się zmieniły:

zmiana + powiadomienie
zakupy.add("Masło")                 // 1. zmiana danych
adapter.notifyDataSetChanged()      // 2. „dane się zmieniły — narysuj listę od nowa”
zakupy.add(…)
lista w pamięci
ma nowy element
→
notifyDataSetChanged()
adapter dowiaduje się
o zmianie
→
ListView
rysuje wiersze
od nowa

Zasada jest prosta: po każdej zmianie listy — notifyDataSetChanged(). Najwygodniej wywoływać ją w jednym miejscu, w metodzie pokazListe(), razem z odświeżeniem licznika czy komunikatu.

TEORIA

Operacje na liście

ZapisEfekt
zakupy.add("Masło")Dopisuje na końcu.
zakupy.add(0, "Masło")Wstawia na początek (pozycja 0).
zakupy[2] = "Gruszki"Zamienia element nr 2.
zakupy.removeAt(2)Usuwa element nr 2 i zwraca go.
zakupy.remove("Mleko")Usuwa pierwszy element o takiej wartości.
zakupy.removeAll { it.startsWith("✓") }Usuwa wszystkie spełniające warunek.
zakupy.sort()Sortuje alfabetycznie w miejscu.
zakupy.clear()Usuwa wszystko.
zakupy.contains("Mleko")Czy element jest na liście.
TEORIA

Długie przytrzymanie wiersza

Kliknięcie wiersza zwykle robi coś łagodnego (otwiera, zaznacza). Usuwanie przypisujemy długiemu przytrzymaniu — trudniej je wywołać przypadkiem. Działa jak setOnLongClickListener przycisku z lekcji 05, tylko dostaje numer wiersza:

przytrzymanie wiersza
lista.setOnItemLongClickListener { _, _, position, _ ->
    zapytajOUsuniecie(position)
    true                         // przytrzymanie obsłużone — nie traktuj go jak kliknięcia
}
TEORIA

Pusta lista

Pusty ekran wygląda jak błąd. ListView potrafi sam pokazać inną kontrolkę, gdy nie ma żadnych wierszy — wystarczy mu ją wskazać:

komunikat dla pustej listy
lista.emptyView = tvPusta       // tvPusta: TextView „Lista jest pusta”

Od tej chwili ListView sam chowa i pokazuje napis — przy pustej liście widać napis, przy niepustej — wiersze. Napis musi być w układzie obok listy (na tym samym poziomie).

PRZYKŁAD

Lista zakupów

Pole i przycisk Dodaj dopisują produkt (bez duplikatów, wielka litera na początku). Kliknięcie wiersza oznacza produkt jako kupiony (✓) albo cofa oznaczenie. Przytrzymanie wiersza pyta o usunięcie, a po usunięciu Snackbar pozwala cofnąć. Sortuj układa listę alfabetycznie, a Usuń kupione usuwa wszystkie odhaczone. Przy pustej liście widać komunikat.

17:20
Lista zakupów
masło
Dodaj
4 produkty · kupione: 1
Chleb
✓ Mleko
Jabłka
Masło
Sortuj
Usuń kupione
dodano „Masło”, mleko oznaczone
→przytrzymanie
„Chleb”
i Usuń
17:21
Lista zakupów
Produkt
Dodaj
3 produkty · kupione: 1
✓ Mleko
Jabłka
Masło
Sortuj
Usuń kupione
Usunięto: Chleb COFNIJ
po usunięciu — Snackbar z cofnięciem
17:30
Lista zakupów
Produkt
Dodaj
0 produktów · kupione: 0
🛒
Lista jest pusta.
Dodaj pierwszy produkt.
pusta lista — emptyView

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa ListaZakupow, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt.
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="Lista zakupów"
        android:textSize="24sp"
        android:textStyle="bold" />

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

        <EditText
            android:id="@+id/etProdukt"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Produkt"
            android:imeOptions="actionDone"
            android:inputType="textCapSentences" />

        <Button
            android:id="@+id/btnDodaj"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Dodaj" />
    </LinearLayout>

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

    <!-- lista i komunikat o pustej liście zajmują to samo miejsce -->
    <ListView
        android:id="@+id/lista"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <TextView
        android:id="@+id/tvPusta"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:text="🛒\nLista jest pusta.\nDodaj pierwszy produkt."
        android:textSize="16sp" />

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

        <Button
            android:id="@+id/btnSortuj"
            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="Sortuj" />

        <Button
            android:id="@+id/btnUsunKupione"
            style="@style/Widget.Material3.Button.OutlinedButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Usuń kupione" />
    </LinearLayout>

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

import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
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 etProdukt: EditText
    private lateinit var btnDodaj: Button
    private lateinit var btnSortuj: Button
    private lateinit var btnUsunKupione: Button
    private lateinit var tvLicznik: TextView
    private lateinit var tvPusta: TextView
    private lateinit var lista: ListView

    // ---------- dane ----------
    private val KUPIONE = "✓ "                                  // znacznik na początku nazwy
    private val zakupy = mutableListOf("Chleb", "Mleko", "Jabłka")
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUI()
        adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, zakupy)
        lista.adapter = adapter
        lista.emptyView = tvPusta
        ustawZdarzenia()
        pokazListe()
    }

    private fun initUI() {
        glownyUklad = findViewById(R.id.main)
        etProdukt = findViewById(R.id.etProdukt)
        btnDodaj = findViewById(R.id.btnDodaj)
        btnSortuj = findViewById(R.id.btnSortuj)
        btnUsunKupione = findViewById(R.id.btnUsunKupione)
        tvLicznik = findViewById(R.id.tvLicznik)
        tvPusta = findViewById(R.id.tvPusta)
        lista = findViewById(R.id.lista)
    }

    private fun ustawZdarzenia() {
        btnDodaj.setOnClickListener { dodaj() }
        btnSortuj.setOnClickListener { sortuj() }
        btnUsunKupione.setOnClickListener { usunKupione() }

        lista.setOnItemClickListener { _, _, position, _ ->
            przelaczKupione(position)
        }
        lista.setOnItemLongClickListener { _, _, position, _ ->
            zapytajOUsuniecie(position)
            true
        }
    }

    // ---------- czynności ----------
    private fun dodaj() {
        val nazwa = etProdukt.text.toString().trim().replaceFirstChar { it.uppercase() }
        if (nazwa.isEmpty()) {
            etProdukt.error = "Wpisz nazwę produktu"
            return
        }
        if (zakupy.contains(nazwa) || zakupy.contains(KUPIONE + nazwa)) {
            etProdukt.error = "Ten produkt już jest na liście"
            return
        }
        zakupy.add(nazwa)
        etProdukt.setText("")
        pokazListe()
        lista.smoothScrollToPosition(zakupy.lastIndex)    // przewiń do nowego wiersza
    }

    private fun przelaczKupione(nr: Int) {
        val nazwa = zakupy[nr]
        zakupy[nr] = if (nazwa.startsWith(KUPIONE)) {
            nazwa.removePrefix(KUPIONE)
        } else {
            KUPIONE + nazwa
        }
        pokazListe()
    }

    private fun zapytajOUsuniecie(nr: Int) {
        AlertDialog.Builder(this)
            .setTitle("Usunąć „${zakupy[nr]}”?")
            .setPositiveButton("Usuń") { _, _ -> usun(nr) }
            .setNegativeButton("Anuluj", null)
            .show()
    }

    private fun usun(nr: Int) {
        val usuniety = zakupy.removeAt(nr)
        pokazListe()

        Snackbar.make(glownyUklad, "Usunięto: ${usuniety.removePrefix(KUPIONE)}", Snackbar.LENGTH_LONG)
            .setAction("Cofnij") {
                zakupy.add(nr, usuniety)                    // z powrotem na to samo miejsce
                pokazListe()
            }
            .show()
    }

    private fun sortuj() {
        // kupione na końcu, w obu grupach alfabetycznie
        zakupy.sortWith(compareBy({ it.startsWith(KUPIONE) }, { it.removePrefix(KUPIONE) }))
        pokazListe()
    }

    private fun usunKupione() {
        val ile = zakupy.count { it.startsWith(KUPIONE) }
        if (ile == 0) return
        zakupy.removeAll { it.startsWith(KUPIONE) }
        pokazListe()
        Snackbar.make(glownyUklad, "Usunięto kupione: $ile", Snackbar.LENGTH_SHORT).show()
    }

    // ---------- dane → ekran ----------
    private fun pokazListe() {
        adapter.notifyDataSetChanged()                      // po KAŻDEJ zmianie listy

        val kupione = zakupy.count { it.startsWith(KUPIONE) }
        tvLicznik.text = "Produktów: ${zakupy.size} · kupione: $kupione"
        btnSortuj.isEnabled = zakupy.size > 1
        btnUsunKupione.isEnabled = kupione > 0
    }
}

Co dzieje się po kolei

  • Lista zakupy i adapter są polami. Adapter powstaje w onCreate — po initUI(), bo potrzebuje aktywności jako kontekstu.
  • lista.emptyView = tvPusta: lista i napis zajmują w układzie to samo miejsce, a ListView sam pokazuje jedno z nich.
  • dodaj() sprawdza pusty tekst i duplikaty (także w wersji z ✓), dopisuje produkt, czyści pole i przewija listę do nowego wiersza. replaceFirstChar { it.uppercase() } zamienia pierwszą literę na wielką.
  • Kliknięcie dodaje albo usuwa znacznik „✓ ” na początku nazwy — zmienia element listy przez zakupy[nr] = ….
  • Usuwanie zapamiętuje usunięty element i jego numer. „Cofnij” wstawia go z powrotem w to samo miejsce przez add(nr, …).
  • sortWith(compareBy(…)) sortuje według dwóch kryteriów: najpierw niekupione (false przed true), potem alfabetycznie po nazwie bez znacznika.
  • Każda metoda kończy się wywołaniem pokazListe(), a ta zawsze zaczyna od notifyDataSetChanged(). Nie da się „zapomnieć” o odświeżeniu.

Sprawdź trzy rzeczy

  1. Zakomentuj w pokazListe() linię notifyDataSetChanged() i dodaj produkt. Licznik wzrośnie, ale nowego wiersza nie będzie — lista w pamięci i na ekranie się rozjechały.
  2. Usuń wszystkie produkty. Pojawi się koszyk z komunikatem. Dodaj produkt — komunikat zniknie sam.
  3. Usuń drugi wiersz, a potem kliknij „Cofnij”. Produkt wróci na drugie miejsce, nie na koniec.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
mutableListOf(…)MutableListLista, do której można dodawać i z której można usuwać.
add, add(nr, …), removeAt, removeAll { }, clearmetodyZmiany zawartości listy.
lista[nr] = …operatorZamiana elementu.
sort(), sortWith(compareBy(…))metodySortowanie w miejscu, także po kilku kryteriach.
adapter.notifyDataSetChanged()metodaOdświeża ListView po zmianie danych.
setOnItemLongClickListener { _, _, position, _ -> …; true }metodaPrzytrzymanie wiersza.
lista.emptyView = widokwłaściwośćWidok pokazywany przy pustej liście.
lista.smoothScrollToPosition(nr)metodaPłynnie przewija do wiersza.
napis.startsWith(…), removePrefix(…)metodySprawdzenie i usunięcie początku napisu.
napis.replaceFirstChar { it.uppercase() }StringWielka pierwsza litera.
android:imeOptions="actionDone"atrybutKlawisz „Gotowe” na klawiaturze.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
dodany element nie pojawia się na liścieBrak notifyDataSetChanged() po zmianie.
arrayOf(…) i próba addBłąd kompilacji — tablica nie ma add. Użyj mutableListOf.
listOf(…) i próba addBłąd kompilacji — listOf tworzy listę tylko do odczytu.
zakupy = mutableListOf(…) przy czyszczeniuPole wskazuje nową listę, a adapter pracuje na starej — ekran przestaje się zmieniać. Czyść przez zakupy.clear(), a pole deklaruj jako val.
przytrzymanie usuwa, a potem jeszcze oznacza wierszLambda setOnItemLongClickListener zwraca false. Ostatnia linia: true.
„Cofnij” wstawia element na koniecUżyto add(usuniety). Zapamiętaj numer i użyj add(nr, usuniety).
komunikat pustej listy widoczny zawszeBrak lista.emptyView = tvPusta — wtedy nikt go nie chowa.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w układzie z lekcji 19. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Lista gości★☆☆

Pole i przycisk dopisują imię gościa na urodziny. Kliknięcie gościa usuwa go od razu (bez pytania) i pokazuje Toast „Usunięto: Ola”. Nad listą licznik „Gości: 7”.

pokaż wskazówkę
  • val imie = goscie.removeAt(position) — removeAt zwraca usunięty element.
ZAD 2Kolejka do lekarza★☆☆

Pacjenci dopisywani na koniec. Przycisk Następny usuwa pierwszego z kolejki i pokazuje go w napisie „Teraz przyjmowany: …”. Przycisk Pilny dodaje pacjenta na początek kolejki. Pusta kolejka — komunikat i wyłączony Następny.

pokaż wskazówkę
  • Na początek: add(0, …), z początku: removeAt(0).
ZAD 3Edycja przez okno★★☆

Rozbuduj przykład: przytrzymanie wiersza otwiera okno z listą akcji (lekcja 23): „Zmień nazwę”, „Przenieś na górę”, „Usuń”. Zmiana nazwy otwiera okno z polem tekstowym wypełnionym obecną nazwą (lekcja 24).

pokaż wskazówkę
  • Przeniesienie na górę: val e = zakupy.removeAt(nr); zakupy.add(0, e). Pamiętaj o pokazListe() po każdej akcji.
ZAD 4Historia kalkulatora★★☆

Prosty kalkulator dwóch liczb z czterema działaniami. Każde obliczenie dopisuje się na górę listy historii: „12 × 7 = 84”. Historia ma najwyżej 10 wpisów — najstarszy znika. Kliknięcie wpisu wstawia jego wynik do pierwszego pola.

pokaż wskazówkę
  • Po add(0, wpis) sprawdź if (historia.size > 10) historia.removeAt(historia.lastIndex). Wynik z wpisu: wpis.substringAfter("= ").
ZAD 5Wyszukiwanie na żywo★★☆

Lista 20 miast. Nad nią pole „Szukaj…”. Przy każdej zmianie tekstu lista pokazuje tylko pasujące miasta. Pełna lista jest w osobnym polu i nie zmienia się — adapter pracuje na drugiej liście, „widocznej”, którą czyścisz i wypełniasz od nowa.

pokaż wskazówkę
  • et.addTextChangedListener { filtruj(it.toString()) }; w filtruj: widoczne.clear(); widoczne.addAll(wszystkie.filter { … }); adapter.notifyDataSetChanged().
ZAD 6Dwie listy: do zrobienia i zrobione★★★

Dwie listy jedna pod drugą (po połowie ekranu): „Do zrobienia” i „Zrobione”. Kliknięcie zadania przenosi je na drugą listę i odwrotnie. Każda lista ma własny adapter, własny licznik i własny komunikat pustej listy. Przytrzymanie usuwa zadanie z cofnięciem w Snackbarze — cofnięcie przywraca je na właściwą listę i właściwe miejsce.

pokaż wskazówkę
  • Dwie listy, dwa adaptery, jedna metoda pokazListy() wywołująca notifyDataSetChanged() na obu. Przy usuwaniu zapamiętaj trzy rzeczy: element, numer i to, z której listy pochodził.
PODSUMOWANIE

Co trzeba zapamiętać

  • Lista, którą można zmieniać: private val dane = mutableListOf(…); adapter jako pole.
  • Po każdej zmianie listy: adapter.notifyDataSetChanged() — najlepiej w jednej metodzie pokazListe().
  • Nie podmieniaj listy (dane = …) — zmieniaj jej zawartość (clear, add, removeAt).
  • Usuwanie pod przytrzymaniem, z potwierdzeniem lub cofnięciem.
  • lista.emptyView pokazuje komunikat, gdy lista jest pusta.

Dokumentacja: ArrayAdapter.notifyDataSetChanged, Operacje na listach w Kotlinie.

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