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.
Czego się dziś nauczysz
- Podłączysz do
ListViewlistęMutableListzamiast 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.
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”):
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).
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:
zakupy.add("Masło") // 1. zmiana danych
adapter.notifyDataSetChanged() // 2. „dane się zmieniły — narysuj listę od nowa”
ma nowy element
o zmianie
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.
Operacje na liście
| Zapis | Efekt |
|---|---|
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. |
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:
lista.setOnItemLongClickListener { _, _, position, _ ->
zapytajOUsuniecie(position)
true // przytrzymanie obsłużone — nie traktuj go jak kliknięcia
}
Pusta lista
Pusty ekran wygląda jak błąd. ListView potrafi sam pokazać inną kontrolkę, gdy nie ma żadnych wierszy — wystarczy mu ją wskazać:
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).
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.
„Chleb”
i Usuń
Lista jest pusta.
Dodaj pierwszy produkt.
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
ListaZakupow, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt.
<?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>
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
zakupyi adapter są polami. Adapter powstaje wonCreate— poinitUI(), bo potrzebuje aktywności jako kontekstu. lista.emptyView = tvPusta: lista i napis zajmują w układzie to samo miejsce, aListViewsam 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 (falseprzedtrue), potem alfabetycznie po nazwie bez znacznika.- Każda metoda kończy się wywołaniem
pokazListe(), a ta zawsze zaczyna odnotifyDataSetChanged(). Nie da się „zapomnieć” o odświeżeniu.
Sprawdź trzy rzeczy
- 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. - Usuń wszystkie produkty. Pojawi się koszyk z komunikatem. Dodaj produkt — komunikat zniknie sam.
- Usuń drugi wiersz, a potem kliknij „Cofnij”. Produkt wróci na drugie miejsce, nie na koniec.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
mutableListOf(…) | MutableList | Lista, do której można dodawać i z której można usuwać. |
add, add(nr, …), removeAt, removeAll { }, clear | metody | Zmiany zawartości listy. |
lista[nr] = … | operator | Zamiana elementu. |
sort(), sortWith(compareBy(…)) | metody | Sortowanie w miejscu, także po kilku kryteriach. |
adapter.notifyDataSetChanged() | metoda | Odświeża ListView po zmianie danych. |
setOnItemLongClickListener { _, _, position, _ -> …; true } | metoda | Przytrzymanie wiersza. |
lista.emptyView = widok | właściwość | Widok pokazywany przy pustej liście. |
lista.smoothScrollToPosition(nr) | metoda | Płynnie przewija do wiersza. |
napis.startsWith(…), removePrefix(…) | metody | Sprawdzenie i usunięcie początku napisu. |
napis.replaceFirstChar { it.uppercase() } | String | Wielka pierwsza litera. |
android:imeOptions="actionDone" | atrybut | Klawisz „Gotowe” na klawiaturze. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| dodany element nie pojawia się na liście | Brak notifyDataSetChanged() po zmianie. |
arrayOf(…) i próba add | Błąd kompilacji — tablica nie ma add. Użyj mutableListOf. |
listOf(…) i próba add | Błąd kompilacji — listOf tworzy listę tylko do odczytu. |
zakupy = mutableListOf(…) przy czyszczeniu | Pole 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 wiersz | Lambda setOnItemLongClickListener zwraca false. Ostatnia linia: true. |
| „Cofnij” wstawia element na koniec | Użyto add(usuniety). Zapamiętaj numer i użyj add(nr, usuniety). |
| komunikat pustej listy widoczny zawsze | Brak lista.emptyView = tvPusta — wtedy nikt go nie chowa. |
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.
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)—removeAtzwraca usunięty element.
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).
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 opokazListe()po każdej akcji.
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("= ").
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()) }; wfiltruj:widoczne.clear(); widoczne.addAll(wszystkie.filter { … }); adapter.notifyDataSetChanged().
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ącanotifyDataSetChanged()na obu. Przy usuwaniu zapamiętaj trzy rzeczy: element, numer i to, z której listy pochodził.
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 metodziepokazListe(). - Nie podmieniaj listy (
dane = …) — zmieniaj jej zawartość (clear,add,removeAt). - Usuwanie pod przytrzymaniem, z potwierdzeniem lub cofnięciem.
lista.emptyViewpokazuje komunikat, gdy lista jest pusta.
Dokumentacja: ArrayAdapter.notifyDataSetChanged, Operacje na listach w Kotlinie.