ListView i ArrayAdapter
Od najprostszej listy tekstowej, przez stylowanie, aż po własny adapter z ViewHolder i obsługą przycisków w wierszach. Przykłady w Java i Kotlin.
Wybierz język przykładów kodu:
Czym jest ListView i Adapter?
ListView to widok który wyświetla przewijalną listę elementów jeden pod drugim. Adapter to łącznik między danymi a widokiem.
Bez adaptera ListView nie wie co ma wyświetlić. Adapter odpowiada na pytanie: jak ma wyglądać wiersz numer X? – pobiera obiekt, tworzy widok wiersza i zwraca go do ListView.
Oba języki działają na tej samej platformie Android – używają tych samych klas (ListView, ArrayAdapter, LayoutInflater). Różni się tylko składnia. Layouty XML są identyczne dla obu języków.
Podstawowa lista – krok po kroku
Zaczynamy od najprostszego przypadku: lista Stringów z wbudowanym adapterem.
ListView w activity_main.xml
Layout XML jest wspólny dla Java i Kotlin – nie zmienia się.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
ArrayAdapter ze Stringami
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayList<String> owoce = new ArrayList<>(); owoce.add("Jabłko"); owoce.add("Banan"); owoce.add("Gruszka"); ArrayAdapter<String> adapter = new ArrayAdapter<>( this, android.R.layout.simple_list_item_1, owoce ); ListView listView = findViewById(R.id.listView); listView.setAdapter(adapter); } }
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Kotlin: arrayListOf() zamiast new ArrayList<>() val owoce = arrayListOf("Jabłko", "Banan", "Gruszka") // ArrayAdapter działa identycznie jak w Java val adapter = ArrayAdapter( this, android.R.layout.simple_list_item_1, owoce ) val listView: ListView = findViewById(R.id.listView) listView.adapter = adapter // Kotlin: property zamiast setAdapter() } }
android.R.layout.simple_list_item_1 – jeden TextView
android.R.layout.simple_list_item_2 – tekst + podtytuł
android.R.layout.simple_list_item_checked – wiersz z checkboxem
Stylowanie ListView
Wygląd ListView można dostosować atrybutami XML – bez kodu Java ani Kotlin.
<ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" <!-- Tło całej listy --> android:background="#F5F5F5" <!-- Separator między wierszami --> android:divider="#CCCCCC" android:dividerHeight="1dp" <!-- Usunięcie separatora: --> <!-- android:divider="@null" --> <!-- Padding + przewijanie przez padding --> android:paddingTop="8dp" android:paddingBottom="8dp" android:clipToPadding="false" <!-- Podświetlenie kliknięcia --> android:listSelector="#20000000" <!-- Szybkie przewijanie --> android:fastScrollEnabled="true" />
ArrayAdapter z obiektem – toString()
Gdy mamy klasę Osoba i chcemy jej użyć z wbudowanym ArrayAdapter – wystarczy nadpisać toString(). Adapter wywoła ją automatycznie.
toString() nie przyjmuje parametrów. Metoda toString(Osoba o) to inna metoda – adapter wyświetli com.example.Osoba@1a2b3c.
Poprawne toString()
public class Osoba { private String imie; private int wiek; public Osoba(String imie, int wiek) { this.imie = imie; this.wiek = wiek; } public String getImie() { return imie; } public int getWiek() { return wiek; } @Override public String toString() { return imie + " (wiek: " + wiek + ")"; } }
// Kotlin: data class automatycznie generuje toString()! // Ale możemy też nadpisać własną wersję: data class Osoba( val imie: String, val wiek: Int ) { override fun toString(): String { return "$imie (wiek: $wiek)" // string template } }
W Kotlinie data class automatycznie generuje gettery, settery, equals(), hashCode() i toString(). Nie trzeba pisać pól prywatnych ani konstruktora ręcznie. Dostęp do pól: osoba.imie zamiast osoba.getImie().
Własny adapter – krok po kroku
Struktura plików
MainActivity.java← podłączamy adapter
Osoba.java← NOWY: klasa modelu
OsobaAdapter.java← NOWY: własny adapter
res/layout/
activity_main.xml
item_osoba.xml← NOWY: wygląd wiersza
MainActivity.kt← podłączamy adapter
Osoba.kt← NOWY: data class
OsobaAdapter.kt← NOWY: własny adapter
res/layout/
activity_main.xml
item_osoba.xml← NOWY: identyczny jak w Java
Layout wiersza (item_osoba.xml)
Plik XML jest identyczny dla obu języków.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="14dp" android:background="?android:attr/selectableItemBackground" android:focusable="false" android:descendantFocusability="blocksDescendants"> <TextView android:id="@+id/tvImie" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="17sp" android:textStyle="bold" android:textColor="#212121" /> <TextView android:id="@+id/tvWiek" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:textSize="13sp" android:textColor="#757575" /> </LinearLayout>
Kod adaptera
public class OsobaAdapter extends ArrayAdapter<Osoba> { private Context context; public OsobaAdapter(Context context, List<Osoba> osoby) { super(context, 0, osoby); this.context = context; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(context) .inflate(R.layout.item_osoba, parent, false); } Osoba osoba = getItem(position); TextView tvImie = convertView.findViewById(R.id.tvImie); TextView tvWiek = convertView.findViewById(R.id.tvWiek); tvImie.setText(osoba.getImie()); tvWiek.setText("Wiek: " + osoba.getWiek()); return convertView; } }
class OsobaAdapter( context: Context, osoby: List<Osoba> ) : ArrayAdapter<Osoba>(context, 0, osoby) { override fun getView( position: Int, convertView: View?, // ? oznacza nullable – może być null parent: ViewGroup ): View { val view = convertView ?: LayoutInflater.from(context) .inflate(R.layout.item_osoba, parent, false) // ?: (Elvis) = jeśli convertView == null, utwórz nowy widok val osoba = getItem(position)!! // !! = "wiem że nie jest null" val tvImie = view.findViewById<TextView>(R.id.tvImie) val tvWiek = view.findViewById<TextView>(R.id.tvWiek) tvImie.text = osoba.imie // .text property zamiast setText() tvWiek.text = "Wiek: ${osoba.wiek}" // string template return view } }
convertView ?: LayoutInflater... to skrót dla if (convertView != null) convertView else LayoutInflater.... Bardzo często spotykany w adapterach Kotlinowych.
Podłączenie w MainActivity
ArrayList<Osoba> osoby = new ArrayList<>(); osoby.add(new Osoba("Anna Kowalska", 25)); osoby.add(new Osoba("Piotr Nowak", 30)); osoby.add(new Osoba("Kasia Wiśniewska", 22)); OsobaAdapter adapter = new OsobaAdapter(this, osoby); ListView listView = findViewById(R.id.listView); listView.setAdapter(adapter);
val osoby = mutableListOf( Osoba("Anna Kowalska", 25), Osoba("Piotr Nowak", 30), Osoba("Kasia Wiśniewska", 22) ) val adapter = OsobaAdapter(this, osoby) val listView: ListView = findViewById(R.id.listView) listView.adapter = adapter
ViewHolder – optymalizacja adaptera
findViewById() wewnątrz getView() jest kosztowne. ViewHolder rozwiązuje problem – referencje zapisujemy raz przez setTag().
❌ Bez ViewHolder
Każde getView() wywołuje findViewByID() – nawet dla recyklingowanych wierszy.
✅ Z ViewHolder
findViewByID() tylko raz przy tworzeniu. Potem getTag() w O(1).
public class OsobaAdapter extends ArrayAdapter<Osoba> { static class ViewHolder { TextView tvImie; TextView tvWiek; } public OsobaAdapter(Context ctx, List<Osoba> lista) { super(ctx, 0, lista); } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(getContext()) .inflate(R.layout.item_osoba, parent, false); holder = new ViewHolder(); holder.tvImie = convertView.findViewById(R.id.tvImie); holder.tvWiek = convertView.findViewById(R.id.tvWiek); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Osoba osoba = getItem(position); holder.tvImie.setText(osoba.getImie()); holder.tvWiek.setText("Wiek: " + osoba.getWiek()); return convertView; } }
class OsobaAdapter( context: Context, osoby: List<Osoba> ) : ArrayAdapter<Osoba>(context, 0, osoby) { // Kotlin: class wewnątrz class – bez słowa "static" private class ViewHolder( val tvImie: TextView, val tvWiek: TextView ) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view: View val holder: ViewHolder if (convertView == null) { view = LayoutInflater.from(context) .inflate(R.layout.item_osoba, parent, false) holder = ViewHolder( view.findViewById(R.id.tvImie), view.findViewById(R.id.tvWiek) ) view.tag = holder // .tag zamiast setTag() } else { view = convertView holder = view.tag as ViewHolder // as = rzutowanie } val osoba = getItem(position)!! holder.tvImie.text = osoba.imie holder.tvWiek.text = "Wiek: ${osoba.wiek}" return view } }
Button w wierszu listy
Button btnUsun = convertView.findViewById(R.id.btnUsun); btnUsun.setOnClickListener(v -> { Osoba osoba = getItem(position); remove(osoba); notifyDataSetChanged(); });
val btnUsun = view.findViewById<Button>(R.id.btnUsun) // Kotlin: lambda zamiast anonimowej klasy btnUsun.setOnClickListener { val osoba = getItem(position)!! remove(osoba) notifyDataSetChanged() }
Button kradnie dotyk. Rozwiązanie w XML: android:focusable="false" na Buttonie + android:descendantFocusability="blocksDescendants" na kontenerze wiersza.
Kliknięcie wiersza
listView.setOnItemClickListener((parent, view, position, id) -> { Osoba kliknieta = osoby.get(position); String info = "Imię: " + kliknieta.getImie() + "\nWiek: " + kliknieta.getWiek(); Toast.makeText(this, info, Toast.LENGTH_LONG).show(); });
listView.setOnItemClickListener { _, _, position, _ -> // _ = parametr którego nie używamy (parent, view, id) val kliknieta = osoby[position] // [] zamiast .get() val info = "Imię: ${kliknieta.imie}\nWiek: ${kliknieta.wiek}" Toast.makeText(this, info, Toast.LENGTH_LONG).show() }
Java vs Kotlin – najważniejsze różnice
| Temat | Java | Kotlin |
|---|---|---|
| Klasa modelu | class z polami, konstruktorem i getterami | data class – wszystko w jednej linii |
| Lista | new ArrayList<>() | arrayListOf() lub mutableListOf() |
| Null check | if (x == null) | ?: (Elvis), ?. (safe call), !! (not-null) |
| String z wartością | „Wiek: ” + osoba.getWiek() | „Wiek: ${osoba.wiek}” |
| Getter/setter | getImie(), setImie() | osoba.imie (property) |
| setAdapter() | listView.setAdapter(a) | listView.adapter = a |
| setText() | tv.setText(„tekst”) | tv.text = „tekst” |
| setTag() | view.setTag(holder) | view.tag = holder |
| Lambda listener | (p, v, pos, id) -> {} | { _, _, pos, _ -> } (nieużywane = _) |
| Klasa wewnętrzna | static class ViewHolder | private class ViewHolder |
| Dziedziczenie | extends ArrayAdapter | : ArrayAdapter(ctx, 0, lista) |
| Override | @Override + public | override fun (bez @Override) |
Tabela podsumowania
| Element | Co to jest? | Czemu potrzebne? |
|---|---|---|
| ListView | Widok Android | Wyświetla przewijalną listę. Potrzebuje adaptera. |
| ArrayAdapter | Gotowy adapter | Łączy ArrayList z gotowym layoutem. Wywołuje toString(). |
| toString() | Metoda z Object | Nadpisujemy bez parametrów gdy używamy ArrayAdapter z obiektami. |
| data class (Kotlin) | Klasa modelu | Kotlin: automatyczny konstruktor, gettery, toString(), equals(). |
| Własny adapter | Klasa po ArrayAdapter | Gdy wiersz ma obrazki, wiele textów, przyciski lub kolory warunkowe. |
| getView() | Metoda adaptera | Wywoływana dla każdego wiersza: inflate → findView → setText → return. |
| convertView / ?: | Recykling wiersza | Java: if null – nowy widok. Kotlin: Elvis operator ?: |
| ViewHolder | Klasa statyczna | Przechowuje referencje do widoków. Eliminuje wielokrotne findViewByID. |
| setTag()/getTag() | Metody View | Java: setTag(holder). Kotlin: view.tag = holder. |
| setAdapter() | Metoda ListView | Java: setAdapter(a). Kotlin: adapter = a (property). |
| notifyDataSetChanged() | Metoda adaptera | Odświeża ListView po zmianie danych. Identyczna w obu językach. |
| focusable=”false” | Atrybut XML | Bez tego Button blokuje setOnItemClickListener na ListView. |
Zadanie dla uczniów
Zadanie – Lista produktów sklepu
- Stwórz klasę
Produktz polami:nazwa,cena(double),dostepny(boolean) - Stwórz layout wiersza z TextViewami na nazwę i cenę
- Stwórz adapter
ProduktAdapterz wzorcem ViewHolder - Dodaj co najmniej 5 produktów (mix dostępnych i niedostępnych)
- Niedostępny – nazwa na czerwono; dostępny – na zielono
- Kliknięcie wiersza – Toast z nazwą i ceną
⭐ Bonus 1: Button „Kup” zmienia dostepny=false i odświeża listę
⭐⭐ Bonus 2: EditText + Button nad listą dodaje nowy produkt
⭐⭐⭐ Bonus Kotlin: przepisz całość jako data class + elvis operator