ListView هو مكون أساسي في Android لعرض القوائم العمودية، متاح منذ API 1 (Android 1.0). يقوم ListView بإنشاء قائمة من العناصر باستخدام ArrayAdapter ويدعم تخطيطات خلايا مخصصة. دعنا نفهم ما هو ListView، كيف نقوم بتكوينه وما هي المشاكل التي يخفيها هذا المكون القديم ولكن لا يزال مستخدماً. في IT Sectr نواجه ListView في المشاريع القديمة ونحافظ على التوافق مع Android 4.4. للمقارنة مع البديل الحديث، اقرأ المقال عن RecyclerView.
النقاط الرئيسية
ListView هو ViewGroup من حزمة android.widget مصمم لعرض قائمة قابلة للتمرير من العناصر. ينفذ ListView نمط AdapterView: تمر البيانات عبر محول (Adapter)، الذي يحول عناصر البيانات إلى Views تظهر على الشاشة. ListView موجود في SDK Android منذ الإصدار الأول (API 1) ولا يتطلب مكتبات إضافية.
المعمارية لـ ListView أبسط من RecyclerView: LayoutManager مضمن (تمرير عمودي فقط)، رسوم الحركة غائبة، وتضبط الفواصل عبر سمة XML android:divider. يضيف ListView تلقائياً OverScrollView وتمييز التركيز، مما يبسط إنشاء النماذج الأولية. وفقاً لـ Statista (2025)، حوالي 15% من تطبيقات Android في أعلى 100 في Play Store لا تزال تحتوي على ListView — بشكل أساسي مشاريع قديمة مع دعم ضئيل.
كان ListView هو مكون القوائم الرئيسي في Android من 2008 إلى 2014، عندما تم تقديم RecyclerView في Google I/O 2014. على الرغم من كونه مهملاً جزئياً (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 بمعرفات الودجات في التخطيط. SimpleAdapter مناسب للنماذج الأولية السريعة عندما لا تكون فئة محول منفصلة مطلوبة. الجانب السلبي هو عدم وجود كتابة آمنة للأنواع والتعقيد مع المنطق المخصص.
<!-- تخطيط مع 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 عندما نعمل على شاشات قديمة بها أكثر من 50 عنصراً.
ListView يوفر مجموعة من سمات XML للتكوين السريع دون كود: android:divider (لون أو رسم الفاصل)، android:dividerHeight (السُمك)، android:scrollbars (أشرطة التمرير)، android:fastScrollEnabled (التمرير السريع عبر مقبض)، android:choiceMode (وضع التحديد: singleChoice، multipleChoice). تسمح سمة android:entries بملء القائمة من مصفوفة موارد (string-array) دون محول.
| السمة | الوصف | مثال قيمة |
|---|---|---|
| android:divider | لون أو رسم الفاصل | @color/gray_light |
| android:dividerHeight | سُمك الفاصل بالبكسل/النقطة | 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 | تنفيذ يدوي | إلزامي حسب المعمارية |
| الرسوم المتحركة | فقط انتقالات التلاشي | DefaultItemAnimator (add, remove, move, change) |
| التحديثات المفصلة | لا (فقط notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| مستمع النقر | onItemClickListener (مضمن) | عبر ViewHolder (غير مضمن) |
| الأداء | كافٍ للقوائم حتى 50 عنصراً | 60 FPS مستقرة لـ 1000+ عنصر |
الاستنتاج: للمشاريع الجديدة، استخدم RecyclerView. احتفظ بـ ListView فقط في الكود القديم حيث تكون الترحيل مكلفة بشكل غير مبرر. وفقاً لإرشادات 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 قبل تعيين المحول (setAdapter())، وإلا لن يتم أخذهما في الاعتبار. بعد إضافة رأس، تتغير معرفات مواقع العناصر بعدد الرؤوس.
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() فقط يعيد رسم العروض ولكنه لا يحدث البيانات. قم دائماً بتغيير البيانات عبر المحول، ليس مباشرة.
يتم تعيين ارتفاع عنصر ListView في XML تخطيط العنصر. إذا كان التخطيط يستخدم layout_height="wrap_content"، فإن الارتفاع يضبط حسب المحتوى. لارتفاع ثابت لجميع العناصر، استخدم محولاً مخصصاً يضبط LayoutParams في getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). لارتفاعات متنوعة، استخدم RecyclerView مع أنواع عرض مختلفة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.