Android Studio · Java / Kotlin ·INF.04

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.

ListView ArrayAdapter Custom Adapter ViewHolder XML Layout

Wybierz język przykładów kodu:

01

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.

Dane
List<T>
Adapter
ArrayAdapter / własny
Widok wiersza
item_xxx.xml
ListView
na ekranie
Java vs Kotlin w Androidzie

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.

02

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

res/layout/activity_main.xml XML
<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

java/MainActivity.java Java
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);
    }
}
kotlin/MainActivity.kt Kotlin
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()
    }
}
Gotowe layouty Androida

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

03

Stylowanie ListView

Wygląd ListView można dostosować atrybutami XML – bez kodu Java ani Kotlin.

activity_main.xml – atrybuty ListView XML
<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" />
04

ArrayAdapter z obiektem – toString()

Gdy mamy klasę Osoba i chcemy jej użyć z wbudowanym ArrayAdapter – wystarczy nadpisać toString(). Adapter wywoła ją automatycznie.

Częsty błąd

toString() nie przyjmuje parametrów. Metoda toString(Osoba o) to inna metoda – adapter wyświetli com.example.Osoba@1a2b3c.

Poprawne toString()

java/Osoba.java Java
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/Osoba.kt Kotlin
// 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
    }
}
Kotlin: data class

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().

05

Własny adapter – krok po kroku

Struktura plików

java/com/example/apka/
  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
kotlin/com/example/apka/
  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.

res/layout/item_osoba.xml XML
<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

java/OsobaAdapter.java Java
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;
    }
}
kotlin/OsobaAdapter.kt Kotlin
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
    }
}
Kotlin: operator Elvis ?:

convertView ?: LayoutInflater... to skrót dla if (convertView != null) convertView else LayoutInflater.... Bardzo często spotykany w adapterach Kotlinowych.

Podłączenie w MainActivity

java/MainActivity.java Java
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);
kotlin/MainActivity.kt Kotlin
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
06

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

java/OsobaAdapter.java – z ViewHolder Java
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;
    }
}
kotlin/OsobaAdapter.kt – z ViewHolder Kotlin
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
    }
}
07

Button w wierszu listy

OsobaAdapter – obsługa buttona w getView() Java
Button btnUsun = convertView.findViewById(R.id.btnUsun);

btnUsun.setOnClickListener(v -> {
    Osoba osoba = getItem(position);
    remove(osoba);
    notifyDataSetChanged();
});
OsobaAdapter.kt – obsługa buttona w getView() Kotlin
val btnUsun = view.findViewById<Button>(R.id.btnUsun)

// Kotlin: lambda zamiast anonimowej klasy
btnUsun.setOnClickListener {
    val osoba = getItem(position)!!
    remove(osoba)
    notifyDataSetChanged()
}
Pułapka – button blokuje kliknięcie wiersza

Button kradnie dotyk. Rozwiązanie w XML: android:focusable="false" na Buttonie + android:descendantFocusability="blocksDescendants" na kontenerze wiersza.

08

Kliknięcie wiersza

java/MainActivity.java Java
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();
});
kotlin/MainActivity.kt Kotlin
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()
}
09

Java vs Kotlin – najważniejsze różnice

TematJavaKotlin
Klasa modeluclass z polami, konstruktorem i getteramidata class – wszystko w jednej linii
Listanew ArrayList<>()arrayListOf() lub mutableListOf()
Null checkif (x == null)?: (Elvis), ?. (safe call), !! (not-null)
String z wartością„Wiek: ” + osoba.getWiek()„Wiek: ${osoba.wiek}”
Getter/settergetImie(), 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ętrznastatic class ViewHolderprivate class ViewHolder
Dziedziczenieextends ArrayAdapter: ArrayAdapter(ctx, 0, lista)
Override@Override + publicoverride fun (bez @Override)
10

Tabela podsumowania

ElementCo to jest?Czemu potrzebne?
ListViewWidok AndroidWyświetla przewijalną listę. Potrzebuje adaptera.
ArrayAdapterGotowy adapterŁączy ArrayList z gotowym layoutem. Wywołuje toString().
toString()Metoda z ObjectNadpisujemy bez parametrów gdy używamy ArrayAdapter z obiektami.
data class (Kotlin)Klasa modeluKotlin: automatyczny konstruktor, gettery, toString(), equals().
Własny adapterKlasa po ArrayAdapterGdy wiersz ma obrazki, wiele textów, przyciski lub kolory warunkowe.
getView()Metoda adapteraWywoływana dla każdego wiersza: inflate → findView → setText → return.
convertView / ?:Recykling wierszaJava: if null – nowy widok. Kotlin: Elvis operator ?:
ViewHolderKlasa statycznaPrzechowuje referencje do widoków. Eliminuje wielokrotne findViewByID.
setTag()/getTag()Metody ViewJava: setTag(holder). Kotlin: view.tag = holder.
setAdapter()Metoda ListViewJava: setAdapter(a). Kotlin: adapter = a (property).
notifyDataSetChanged()Metoda adapteraOdświeża ListView po zmianie danych. Identyczna w obu językach.
focusable=”false”Atrybut XMLBez tego Button blokuje setOnItemClickListener na ListView.
11

Zadanie dla uczniów

Zadanie – Lista produktów sklepu

  • Stwórz klasę Produkt z polami: nazwa, cena (double), dostepny (boolean)
  • Stwórz layout wiersza z TextViewami na nazwę i cenę
  • Stwórz adapter ProduktAdapter z 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