ListView — componenta de bază Android pentru afișarea listelor verticale, disponibilă de la API 1 (Android 1.0). ListView creează o listă de elemente cu ajutorul ArrayAdapter și suportă machete personalizate de celule. Aflăm ce este ListView, cum să-l configurăm și ce capcane ascunde această componentă veche, dar încă utilizată. În IT Sectr întâlnim ListView în proiecte legacy și suținem compatibilitatea inversă cu Android 4.4. Pentru comparație cu alternativa modernă citiți articolul despre RecyclerView.
Principalele
ListView — este un grup View din pachetul android.widget, destinat afișării unei liste derulabile de elemente. ListView implementează pattern-ul AdapterView: datele sunt transmise prin adaptor (Adapter), care convertește elementele de date în View afișate pe ecran. ListView există în Android SDK de la prima versiune (API 1) și nu necesită biblioteci suplimentare.
Arhitectura ListView este mai simplă decât RecyclerView: LayoutManager este încorporat (doar derulare verticală), animația modificărilor lipsește, separatoarele se stabilesc prin atributul XML android:divider. ListView adaugă automat OverScrollView și evidențiere la focalizare, ceea ce simplifică prototiparea. Conform datelor Statista (2025), aproximativ 15% din aplicațiile Android din top-100 Play Store conțin încă ListView — în principal proiecte legacy cu suport minim.
ListView a fost componenta principală de liste în Android din 2008 până în 2014, când la Google I/O 2014 a fost prezentat RecyclerView. În ciuda statutului parțial învechit (soft-deprecated), ListView rămâne disponibil în toate versiunile Android și nu este marcat ca @Deprecated în SDK API 35. Dacă proiectul dumneavoastră suportă Android 4.4 (API 19) și mai jos, ListView este singura alegere încorporată, deoarece RecyclerView necesită AndroidX și dependințe gradle.
ArrayAdapter — cel mai popular adaptor pentru ListView. Acesta acceptă List<T> și o machetă pentru fiecare element. În mod implicit ArrayAdapter apelează toString() pentru fiecare obiect și afișează textul în simple_list_item_1 (un TextView). Pentru afișare personalizată trebuie suprascris getView() sau transmisă o machetă personalizată în constructor.
// ListView cu ArrayAdapter (text simplu)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"Rusia", "SUA", "Germania",
"Franța", "Japonia", "China", "Brazilia"
)
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
// Gestionarea click-ului
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter este destinat listelor din date Map. Acesta acceptă List<Map<String, Any>> și un tablou from/to pentru legarea cheilor Map cu ID-urile widget-urilor din machetă. SimpleAdapter este comod pentru prototipuri rapide când nu este nevoie de o clasă separată de adaptor. Dezavantaj — lipsa tipizării type-safe și complexitatea cu logică personalizată.
<!-- Machetă cu 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>
Adaptorul personalizat pentru ListView suprascrie metoda getView(), pe care RecyclerView o împarte în onCreateViewHolder + onBindViewHolder. În ListView ambii pași sunt combinați, de aceea dezvoltatorul trebuie să implementeze independent pattern-ul ViewHolder prin convertView (View reutilizabil) și tag setTag().
// Model de date
data class Product(
name: String,
price: String,
imageResId: Int
)
// Adaptor personalizat cu ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder pentru cache
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
}
}
Diferența cheie de RecyclerView: în ListView metoda getView() este apelată pentru fiecare apariție a elementului pe ecran. Dacă nu se utilizează convertView și ViewHolder, fiecare derulare creează un nou obiect View, ceea ce duce la scurgeri de memorie și lag-uri pe liste cu 50+ elemente. ViewHolder cu view.tag — optimizare obligatorie pentru ListView cu machete personalizate.
ListView are patru limitări arhitecturale pe care Google le-a remediat în RecyclerView: (1) doar orientare verticală — fără grile, liste orizontale sau cascade; (2) lipsa notificărilor punctuale — orice modificare a datelor cauzează o redesenare completă prin notifyDataSetChanged(), ceea ce creează sacadări; (3) fără animație încorporată de adăugare/ștergere a elementelor; (4) fără separare a responsabilităților — LayoutManager este încorporat, ItemAnimator lipsește.
<!-- item_product.xml — macheta elementului 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>
Consecințe practice: pe liste de până la 20 de elemente ListView funcționează la fel de rapid ca RecyclerView. La 100+ elemente diferența devine vizibilă: ListView începe să piardă FPS din cauza lipsei calculelor diferențiale și a ViewHolder forțat. Pentru liste cu actualizări frecvente (chat, feed, loguri) ListView nu este potrivit — utilizați RecyclerView cu Paging 3. În IT Sectr migrăm ListView pe RecyclerView la orice lucru pe ecrane legacy cu mai mult de 50 de elemente.
ListView oferă un set de atribute XML pentru configurare rapidă fără cod: android:divider (culoarea/desenul separatorului), android:dividerHeight (grosimea), android:scrollbars (bare de derulare), android:fastScrollEnabled (derulare rapidă prin mâner), android:choiceMode (mod de selecție: singleChoice, multipleChoice). Atributul android:entries permite popularea listei dintr-un tablou de resurse (string-array) fără adaptor.
| Atribut | Descriere | Valoare exemplu |
|---|---|---|
| android:divider | Culoarea sau drawable-ul separatorului | @color/gray_light |
| android:dividerHeight | Grosimea separatorului în px/dp | 1dp |
| android:entries | Tablou de șiruri din resurse | @array/countries |
| android:choiceMode | Mod de selecție a elementului | singleChoice |
| android:fastScrollEnabled | Derulare rapidă | true |
| android:overlapAnchor | Ancoră pentru liste derulante (API 21+) | true |
Mod de selecție: choiceMode="singleChoice" activează stilul RadioButton, la care elementul selectat este evidențiat. choiceMode="multipleChoice" — casete de bifare pentru mai multe elemente. Datele de selecție sunt accesibile prin listView.checkedItemPosition sau listView.checkedItemIds. Important: la choiceMode ListView utilizează StateListDrawable pentru fundal — setați propriul selector dacă albastrul standard nu se potrivește cu designul.
Alegerea între ListView și RecyclerView depinde de cerințele proiectului. RecyclerView este standardul modern, dar ListView poate fi justificat pentru ecrane simple cu costuri minime de întreținere a codului vechi. Comparație după toți parametrii cheie:
| Parametru | ListView | RecyclerView |
|---|---|---|
| Dimensiunea bibliotecii | Încorporat în SDK (≈60 KB) | AndroidX (≈300 KB cu dependențe) |
| Versiunea minimă | API 1 | API 14 (prin AndroidX) |
| Orientare | Doar verticală | Verticală, orizontală, grilă, cascadă |
| ViewHolder | Implementare manuală | Obligatoriu prin arhitectură |
| Animație | Doar tranziții fade | DefaultItemAnimator (add, remove, move, change) |
| Actualizări punctuale | Nu (doar notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| Click listener | onItemClickListener (încorporat) | Prin ViewHolder (fără încorporat) |
| Performanță | Suficientă pentru liste de până la 50 elemente | 60 FPS stabil pentru 1000+ elemente |
Concluzie: pentru proiecte noi utilizați RecyclerView. ListView lăsați doar în codul legacy unde migrarea este nejustificat de costisitoare. Conform ghidurilor Material Design 3 (2026), listele de orice complexitate ar trebui să utilizeze RecyclerView pentru a corespunde standardelor moderne de performanță și accesibilitate.
Întrebări frecvente
ListView nu este marcat ca @Deprecated în Android SDK API 35, dar Google nu recomandă utilizarea lui în proiecte noi (soft-deprecated). Componenta rămâne funcțională în toate versiunile Android. RecyclerView este înlocuitorul oficial din 2014. Dacă începeți un proiect nou — alegeți RecyclerView. Dacă întrețineți unul existent — ListView va continua să funcționeze fără modificări.
ListView suportă metodele addHeaderView(View, Object, boolean) și addFooterView(View, Object, boolean). Header și footer se adaugă în lista generală de elemente, dar nu participă în adaptor — ele se afișează întotdeauna la începutul și sfârșitul listei. Important: adăugați header/footer înainte de setarea adaptorului (setAdapter()), altfel nu vor fi luate în considerare. După adăugarea header, ID-urile pozițiilor elementelor se deplasează cu numărul de header-e.
ListView și ScrollView ambele procesează gesturi verticale, ceea ce creează un conflict: ScrollView exterior interceptează derularea, iar ListView nu se derulează. Soluție: (1) nu încorporați ListView în ScrollView — utilizați RecyclerView cu NestedScrollingChild, (2) înlocuiți ScrollView cu NestedScrollView (API 21+), (3) utilizați ListView cu înălțime fixă (android:layout_height="...dp") în interiorul ScrollView, dar aceasta va sparge adaptabilitatea.
Curățarea datelor în ListView depinde de tipul adaptorului. Pentru ArrayAdapter: adapter.clear() și adapter.addAll(newList) urmate de adapter.notifyDataSetChanged(). Pentru adaptor personalizat: actualizați lista de date și apelați notifyDataSetChanged(). Atenție: listView.invalidateViews() doar redesenează View, dar nu actualizează datele. Modificați întotdeauna datele prin adaptor, nu direct.
Înălțimea elementului ListView se stabilește în macheta item. Dacă macheta utilizează layout_height="wrap_content", înălțimea se ajustează după conținut. Pentru înălțime fixă a tuturor elementelor utilizați un adaptor personalizat care stabilește LayoutParams în getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). Pentru înălțimi eterogene utilizați RecyclerView cu tipuri de vizualizare diferite.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și