ListView — podstawowy komponent Android do wyświetlania pionowych list, dostępny od API 1 (Android 1.0). ListView tworzy listę elementów za pomocą ArrayAdapter i obsługuje niestandardowe układy komórek. Dowiedz się, czym jest ListView, jak go skonfigurować i jakie pułapki kryje ten stary, ale wciąż używany komponent. W IT Sectr spotykamy ListView w legacy-projektach i utrzymujemy wsteczną zgodność z Android 4.4. Porównanie z nowoczesną alternatywą znajdziesz w artykule o RecyclerView.
Najważniejsze
ListView — to grupa View z pakietu android.widget, przeznaczona do wyświetlania przewijanej listy elementów. ListView implementuje wzorzec AdapterView: dane są przekazywane przez adapter (Adapter), który konwertuje elementy danych na View wyświetlane na ekranie. ListView istnieje w Android SDK od pierwszej wersji (API 1) i nie wymaga dodatkowych bibliotek.
Architektura ListView jest prostsza niż RecyclerView: LayoutManager jest wbudowany (tylko pionowe przewijanie), animacja zmian nie występuje, separatory ustawiane są przez atrybut XML android:divider. ListView automatycznie dodaje OverScrollView i podświetlanie przy fokusie, co upraszcza prototypowanie. Według danych Statista (2025), około 15% aplikacji Android w top-100 Play Store wciąż zawiera ListView — głównie legacy-projekty z minimalnym wsparciem.
ListView był głównym komponentem list w Androidzie od 2008 do 2014 roku, kiedy na Google I/O 2014 zaprezentowano RecyclerView. Mimo statusu częściowo przestarzałego (soft-deprecated), ListView pozostaje dostępny we wszystkich wersjach Androida i nie jest oznaczony jako @Deprecated w SDK API 35. Jeśli Twój projekt obsługuje Android 4.4 (API 19) i starsze, ListView jest jedynym wbudowanym wyborem, ponieważ RecyclerView wymaga AndroidX i zależności gradle.
ArrayAdapter — najpopularniejszy adapter dla ListView. Przyjmuje List<T> i układ dla każdego elementu. Domyślnie ArrayAdapter wywołuje toString() dla każdego obiektu i wyświetla tekst w simple_list_item_1 (jeden TextView). Dla niestandardowego wyświetlania należy nadpisać getView() lub przekazać niestandardowy układ w konstruktorze.
// ListView z ArrayAdapter (prosty tekst)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"Rosja", "USA", "Niemcy",
"Francja", "Japonia", "Chiny", "Brazylia"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
countries
)
binding.listView.adapter = adapter
// Obsługa kliknięcia
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter jest przeznaczony dla list z danych Map. Przyjmuje List<Map<String, Any>> i tablicę from/to do wiązania kluczy Map z ID widżetów w układzie. SimpleAdapter jest wygodny dla szybkich prototypów, gdy nie potrzeba osobnej klasy adaptera. Wadą jest brak type-safe typizacji i trudność z niestandardową logiką.
<!-- Układ z ListView -->
<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"
android:divider="@color/divider"
android:dividerHeight="1px"
android:scrollbars="vertical" />
</LinearLayout>
Niestandardowy adapter dla ListView nadpisuje metodę getView(), którą RecyclerView dzieli na onCreateViewHolder + onBindViewHolder. W ListView oba kroki są połączone, więc programista musi samodzielnie zaimplementować wzorzec ViewHolder przez convertView (ponownie wykorzystywane View) i tag setTag().
// Model danych
data class Product(
name: String,
price: String,
imageResId: Int
)
// Niestandardowy adapter z ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder do buforowania
private class ViewHolder(view: View) {
val nameText: TextView = view.findViewById(R.id.tvName)
val priceText: TextView = view.findViewById(R.id.tvPrice)
val imageView: ImageView = view.findViewById(R.id.ivProduct)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.item_product, parent, false)
.also { it.tag = ViewHolder(it) }
val holder = view.tag as ViewHolder
val product = getItem(position)
holder.nameText.text = product.name
holder.priceText.text = product.price
holder.imageView.setImageResource(product.imageResId)
return view
}
}
Kluczowa różnica od RecyclerView: w ListView metoda getView() jest wywoływana przy każdym pojawieniu się elementu na ekranie. Jeśli nie użyjesz convertView i ViewHolder, każde przewinięcie tworzy nowy obiekt View, co prowadzi do wycieków pamięci i opóźnień na listach z 50+ elementami. ViewHolder z view.tag — obowiązkowa optymalizacja dla ListView z niestandardowymi układami.
ListView ma cztery ograniczenia architektoniczne, które Google naprawiło w RecyclerView: (1) tylko pionowa orientacja — brak siatek, poziomych list lub kaskad; (2) brak punktowych powiadomień — każda zmiana danych powoduje pełne przerysowanie przez notifyDataSetChanged(), co powoduje drgania; (3) brak wbudowanej animacji dodawania/usuwania elementów; (4) brak podziału odpowiedzialności — LayoutManager jest wbudowany, ItemAnimator nie istnieje.
<!-- item_product.xml — układ elementu ListView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:textSize="14sp"
android:textColor="@color/price" />
</LinearLayout>
</LinearLayout>
Praktyczne konsekwencje: na listach do 20 elementów ListView działa tak samo szybko jak RecyclerView. Przy 100+ elementach różnica staje się zauważalna: ListView zaczyna tracić FPS z powodu braku obliczeń różnicowych i wymuszonego ViewHolder. Dla list z częstymi aktualizacjami (czat, feed, logi) ListView nie nadaje się — użyj RecyclerView z Paging 3. W IT Sectr migrujemy ListView na RecyclerView przy każdej pracy nad legacy-ekranami z więcej niż 50 elementami.
ListView udostępnia zestaw atrybutów XML do szybkiej konfiguracji bez kodu: android:divider (kolor/rysunek separatora), android:dividerHeight (grubość), android:scrollbars (paski przewijania), android:fastScrollEnabled (szybkie przewijanie przez uchwyt), android:choiceMode (tryb wyboru: singleChoice, multipleChoice). Atrybut android:entries pozwala wypełnić listę z tablicy zasobów (string-array) bez adaptera.
| Atrybut | Opis | Przykładowa wartość |
|---|---|---|
| android:divider | Kolor lub drawable separatora | @color/gray_light |
| android:dividerHeight | Grubość separatora w px/dp | 1dp |
| android:entries | Tablica stringów z zasobów | @array/countries |
| android:choiceMode | Tryb wyboru elementu | singleChoice |
| android:fastScrollEnabled | Szybkie przewijanie | true |
| android:overlapAnchor | Kotwica dla list rozwijanych (API 21+) | true |
Tryb wyboru: choiceMode="singleChoice" włącza styl RadioButton, w którym wybrany element jest podświetlany. choiceMode="multipleChoice" — pola wyboru dla wielu elementów. Dane wyboru są dostępne przez listView.checkedItemPosition lub listView.checkedItemIds. Ważne: przy choiceMode ListView używa StateListDrawable dla tła — ustaw własny selektor, jeśli standardowy niebieski nie pasuje do projektu.
Wybór między ListView a RecyclerView zależy od wymagań projektu. RecyclerView to nowoczesny standard, ale ListView może być uzasadniony dla prostych ekranów z minimalnymi kosztami utrzymania starego kodu. Porównanie według wszystkich kluczowych parametrów:
| Parametr | ListView | RecyclerView |
|---|---|---|
| Rozmiar biblioteki | Wbudowany w SDK (≈60 KB) | AndroidX (≈300 KB z zależnościami) |
| Minimalna wersja | API 1 | API 14 (przez AndroidX) |
| Orientacja | Tylko pionowa | Pionowa, pozioma, siatka, kaskada |
| ViewHolder | Ręczna implementacja | Obowiązkowy z założenia |
| Animacja | Tylko przejścia fade | DefaultItemAnimator (add, remove, move, change) |
| Aktualizacje punktowe | Nie (tylko notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| Click listener | onItemClickListener (wbudowany) | Przez ViewHolder (brak wbudowanego) |
| Wydajność | Wystarczająca dla list do 50 elementów | Stabilne 60 FPS dla 1000+ elementów |
Podsumowanie: dla nowych projektów używaj RecyclerView. ListView pozostaw tylko w legacy-kodzie, gdzie migracja jest nieuzasadniona kosztowna. Według wytycznych Material Design 3 (2026), listy dowolnej złożoności powinny używać RecyclerView, aby spełniać nowoczesne standardy wydajności i dostępności.
Często zadawane pytania
ListView nie jest oznaczone jako @Deprecated w Android SDK API 35, ale Google nie zaleca jego używania w nowych projektach (soft-deprecated). Komponent pozostaje działający we wszystkich wersjach Androida. RecyclerView jest oficjalnym zamiennikiem od 2014 roku. Jeśli zaczynasz nowy projekt — wybierz RecyclerView. Jeśli utrzymujesz istniejący — ListView będzie działać bez zmian.
ListView obsługuje metody addHeaderView(View, Object, boolean) i addFooterView(View, Object, boolean). Header i footer są dodawane do ogólnej listy elementów, ale nie uczestniczą w adapterze — są zawsze wyświetlane na początku i końcu listy. Ważne: dodawaj header/footer przed ustawieniem adaptera (setAdapter()), w przeciwnym razie nie zostaną uwzględnione. Po dodaniu header ID pozycji elementów przesuwają się o liczbę headerów.
ListView i ScrollView oba obsługują pionowe gesty, co powoduje konflikt: zewnętrzny ScrollView przechwytuje przewijanie, a ListView się nie przewija. Rozwiązanie: (1) nie umieszczaj ListView w ScrollView — użyj RecyclerView z NestedScrollingChild, (2) zastąp ScrollView na NestedScrollView (API 21+), (3) użyj ListView ze stałą wysokością (android:layout_height="...dp") wewnątrz ScrollView, ale to zepsuje responsywność.
Czyszczenie danych w ListView zależy od typu adaptera. Dla ArrayAdapter: adapter.clear() i adapter.addAll(newList) z następującym adapter.notifyDataSetChanged(). Dla niestandardowego adaptera: zaktualizuj listę danych i wywołaj notifyDataSetChanged(). Uwaga: listView.invalidateViews() tylko przerysowuje View, ale nie aktualizuje danych. Zawsze zmieniaj dane przez adapter, a nie bezpośrednio.
Wysokość elementu ListView jest ustawiana w układzie item-rozmieszczenia. Jeśli układ używa layout_height="wrap_content", wysokość dostosowuje się do zawartości. Dla stałej wysokości wszystkich elementów użyj niestandardowego adaptera, który ustawia LayoutParams w getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). Dla różnorodnej wysokości użyj RecyclerView z różnymi typami widoków.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również