ListView — основен Android компонент за показване на вертикални списъци, достъпен от API 1 (Android 1.0). ListView създава списък от елементи с помощта на ArrayAdapter и поддържа персонализирани оформления на клетки. Разбираме какво е ListView, как да го конфигурираме и какви капани крие този стар, но все още използван компонент. В IT Sectr срещаме ListView в legacy проекти и поддържаме обратна съвместимост с Android 4.4. За сравнение с модерната алтернатива прочетете статията за RecyclerView.
Основни точки
ListView — е View група от пакета android.widget, предназначена за показване на превъртаем списък от елементи. ListView имплементира шаблона AdapterView: данните се предават чрез адаптер (Adapter), който преобразува елементите от данни във View, показвани на екрана. ListView съществува в Android SDK от първата версия (API 1) и не изисква допълнителни библиотеки.
Архитектура на ListView е по-проста от RecyclerView: LayoutManager е вграден (само вертикално превъртане), анимация на промени липсва, разделителите се задават чрез XML атрибута android:divider. ListView автоматично добавя OverScrollView и подчертаване при фокус, което опростява прототипирането. Според данни на Statista (2025), около 15% от Android приложенията в топ-100 на Play Store все още съдържат ListView — основно legacy проекти с минимална поддръжка.
ListView беше основният компонент за списъци в Android от 2008 до 2014 г., когато на Google I/O 2014 беше представен RecyclerView. Въпреки статуса на частично остарял (soft-deprecated), ListView остава достъпен във всички версии на Android и не е маркиран като @Deprecated в SDK API 35. Ако вашият проект поддържа Android 4.4 (API 19) и по-ниски, ListView е единственият вграден избор, тъй като RecyclerView изисква AndroidX и gradle зависимости.
ArrayAdapter — най-популярният адаптер за ListView. Той приема List<T> и оформление за всеки елемент. По подразбиране ArrayAdapter извиква toString() за всеки обект и показва текста в simple_list_item_1 (един TextView). За персонализирано показване трябва да се презапише getView() или да се предаде персонализирано оформление в конструктора.
// ListView с ArrayAdapter (прост текст)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"Русия", "САЩ", "Германия",
"Франция", "Япония", "Китай", "Бразилия"
)
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
// Обработка на кликване
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter е предназначен за списъци от Map данни. Той приема List<Map<String, Any>> и масив from/to за свързване на ключовете на Map с ID-тата на джаджите в оформлението. SimpleAdapter е удобен за бързи прототипи, когато не е нужен отделен клас адаптер. Недостатък — липса на type-safe типизация и сложност с персонализирана логика.
<!-- Оформление с 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>
Персонализиран адаптер за ListView презаписва метода getView(), който RecyclerView разделя на onCreateViewHolder + onBindViewHolder. В ListView и двете стъпки са комбинирани, затова разработчикът трябва самостоятелно да имплементира шаблона ViewHolder чрез convertView (повторно използвано View) и етикет setTag().
// Модел на данни
data class Product(
name: String,
price: String,
imageResId: Int
)
// Персонализиран адаптер с ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder за кеширане
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
}
}
Ключова разлика от RecyclerView: в ListView методът getView() се извиква за всяко появяване на елемент на екрана. Ако не се използват convertView и ViewHolder, всяко превъртане създава нов обект View, което води до изтичане на памет и забавяния на списъци с 50+ елемента. ViewHolder с view.tag — задължителна оптимизация за ListView с персонализирани оформления.
ListView има четири архитектурни ограничения, които Google поправи в RecyclerView: (1) само вертикална ориентация — без мрежи, хоризонтални списъци или каскади; (2) липса на точкови известия — всяка промяна на данни причинява пълно прерисуване чрез notifyDataSetChanged(), което създава потрепвания; (3) няма вградена анимация за добавяне/премахване на елементи; (4) няма разделение на отговорностите — LayoutManager е вграден, ItemAnimator липсва.
<!-- item_product.xml — оформление на елемент на 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>
Практически последствия: на списъци до 20 елемента ListView работи толкова бързо, колкото RecyclerView. При 100+ елемента разликата става забележима: ListView започва да губи FPS поради липса на диференциални изчисления и принудителен ViewHolder. За списъци с чести обновявания (чат, фийд, логове) ListView не е подходящ — използвайте RecyclerView с Paging 3. В IT Sectr мигрираме ListView към RecyclerView при всяка работа по legacy екрани с повече от 50 елемента.
ListView предоставя набор от XML атрибути за бърза конфигурация без код: android:divider (цвят/рисунка на разделител), android:dividerHeight (дебелина), android:scrollbars (ленти за превъртане), android:fastScrollEnabled (бързо превъртане чрез дръжка), android:choiceMode (режим на избор: singleChoice, multipleChoice). Атрибутът android:entries позволява попълване на списъка от масив с ресурси (string-array) без адаптер.
| Атрибут | Описание | Примерна стойност |
|---|---|---|
| android:divider | Цвят или drawable на разделител | @color/gray_light |
| android:dividerHeight | Дебелина на разделител в px/dp | 1dp |
| android:entries | Масив от низове от ресурси | @array/countries |
| android:choiceMode | Режим на избор на елемент | singleChoice |
| android:fastScrollEnabled | Бързо превъртане | true |
| android:overlapAnchor | Котва за изскачащи списъци (API 21+) | true |
Режим на избор: choiceMode="singleChoice" активира стил RadioButton, при който избраният елемент се подчертава. choiceMode="multipleChoice" — квадратчета за отметка за множество елементи. Данните за избор са достъпни чрез listView.checkedItemPosition или listView.checkedItemIds. Важно: при choiceMode ListView използва StateListDrawable за фон — задайте собствен селектор, ако стандартният син не подхожда на дизайна.
Избор между ListView и RecyclerView зависи от изискванията на проекта. RecyclerView е модерният стандарт, но ListView може да бъде оправдан за прости екрани с минимални разходи за поддръжка на стар код. Сравнение по всички ключови параметри:
| Параметър | ListView | RecyclerView |
|---|---|---|
| Размер на библиотека | Вграден в SDK (≈60 KB) | AndroidX (≈300 KB със зависимости) |
| Минимална версия | API 1 | API 14 (чрез AndroidX) |
| Ориентация | Само вертикална | Вертикална, хоризонтална, мрежа, каскада |
| ViewHolder | Ръчна имплементация | Задължителен по архитектура |
| Анимация | Само fade преходи | DefaultItemAnimator (add, remove, move, change) |
| Точкови обновявания | Не (само notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| Click listener | onItemClickListener (вграден) | Чрез ViewHolder (няма вграден) |
| Производителност | Достатъчен за списъци до 50 елемента | Стабилни 60 FPS за 1000+ елемента |
Заключение: за нови проекти използвайте RecyclerView. Оставете ListView само в legacy код, където миграцията е неоправдано скъпа. Според насоките на Material Design 3 (2026), списъци с всякаква сложност трябва да използват RecyclerView за съответствие с модерните стандарти за производителност и достъпност.
Често задавани въпроси
ListView не е маркиран като @Deprecated в Android SDK API 35, но Google не препоръчва използването му в нови проекти (soft-deprecated). Компонентът остава работещ във всички версии на Android. RecyclerView е официалният заместител от 2014 г. Ако започвате нов проект — изберете RecyclerView. Ако поддържате съществуващ — ListView ще продължи да работи без промени.
ListView поддържа методите addHeaderView(View, Object, boolean) и addFooterView(View, Object, boolean). Header и footer се добавят към общия списък от елементи, но не участват в адаптера — те винаги се показват в началото и края на списъка. Важно: добавяйте header/footer преди настройване на адаптера (setAdapter()), в противен случай няма да бъдат взети предвид. След добавяне на header, ID-тата на позициите на елементите се изместват с броя на header-ите.
ListView и ScrollView и двата обработват вертикални жестове, което създава конфликт: външният ScrollView прихваща превъртането и ListView не се превърта. Решение: (1) не влагайте ListView в ScrollView — използвайте RecyclerView с NestedScrollingChild, (2) заменете ScrollView с NestedScrollView (API 21+), (3) използвайте ListView с фиксирана височина (android:layout_height="...dp") вътре в ScrollView, но това ще счупи адаптивността.
Изчистването на данни в ListView зависи от типа на адаптера. За ArrayAdapter: adapter.clear() и adapter.addAll(newList) последвано от adapter.notifyDataSetChanged(). За персонализиран адаптер: обновете списъка с данни и извикайте notifyDataSetChanged(). Внимание: listView.invalidateViews() само прерисува View, но не обновява данните. Винаги променяйте данните чрез адаптера, а не директно.
Височината на елемент на ListView се задава в оформлението на item. Ако оформлението използва layout_height="wrap_content", височината се нагажда според съдържанието. За фиксирана височина на всички елементи използвайте персонализиран адаптер, който задава LayoutParams в getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). За разнородна височина използвайте RecyclerView с различни типове изгледи.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също