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 از نسخه اول (API 1) در Android SDK وجود دارد و به کتابخانههای اضافی نیاز ندارد.
معماری ListView سادهتر از RecyclerView است: LayoutManager داخلی است (فقط پیمایش عمودی)، انیمیشن تغییرات وجود ندارد، جداکنندهها از طریق ویژگی XML android:divider تنظیم میشوند. ListView به طور خودکار OverScrollView و برجستهسازی در فوکوس را اضافه میکند که نمونهسازی اولیه را سادهتر میکند. طبق دادههای Statista (2025)، حدود 15% از برنامههای Android در top-100 Play Store هنوز حاوی ListView هستند — عمدتاً پروژههای legacy با پشتیبانی حداقلی.
ListView از 2008 تا 2014 کامپوننت اصلی لیستها در Android بود، تا زمانی که در Google I/O 2014 RecyclerView معرفی شد. علیرغم وضعیت نیمهمنسوخ (soft-deprecated)، ListView در تمام نسخههای Android در دسترس باقی میماند و در SDK API 35 به عنوان @Deprecated علامتگذاری نشده است. اگر پروژه شما از 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 به دلیل عدم محاسبات تفاضلی و ViewHolder اجباری شروع به افت FPS میکند. برای لیستهای با بهروزرسانی مکرر (چت، فید، لاگها) 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 کیلوبایت) | 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 در Android SDK API 35 به عنوان @Deprecated علامتگذاری نشده است، اما 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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید