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 и градл-зависимости.
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 | Anchor для всплывающих списков (API 21+) | true |
Режим выбора: choiceMode="singleChoice" включает RadioButton-стиль, при котором выбранный элемент подсвечивается. choiceMode="multipleChoice" — чекбоксы для нескольких элементов. Данные выбора доступны через listView.checkedItemPosition или listView.checkedItemIds. Важно: при choiceMode ListView использует StateListDrawable для фона — установите собственный селектор, если стандартный синий не подходит под дизайн.
Выбор между ListView и RecyclerView зависит от требований проекта. RecyclerView — современный стандарт, но ListView может быть оправдан для простых экранов с минимальными затратами на поддержку старого кода. Сравнение по всем ключевым параметрам:
| Параметр | ListView | RecyclerView |
|---|---|---|
| Размер библиотеки | Встроен в SDK (≈60 КБ) | AndroidX (≈300 КБ с зависимостями) |
| Минимальная версия | 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 с different view types.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.