واجهة أندرويد والأنشطة والتنقّل

RecyclerView والقوائم

18 دقيقة الدرس 4 من 12

RecyclerView والقوائم

كل تطبيق أندرويد حقيقي يعرض قوائم: خلاصة رسائل، كتالوج منتجات، سجل معاملات. المكوّن المصمَّم لهذه المهمة هو RecyclerView. حلّ محل ListView القديم لأنه يحلّ مشكلتين صعبتين في آن واحد: إنه يُعيد استخدام كائنات الواجهة التي تختفي عن الشاشة بدلاً من تدميرها وإعادة إنشائها، ولا يفرض أي قيود على كيفية ترتيب العناصر أو تحريكها. إن فهم آلية عمل RecyclerView — ولماذا بُني بهذه الطريقة — سيجعلك مطوّر أندرويد أكثر كفاءة في كل ميزة تبنيها.

لماذا وُجد RecyclerView

تخيّل عرض 10,000 جهة اتصال. إنشاء 10,000 كائن View دفعة واحدة سيستهلك مئات الميغابايت من الذاكرة ويُجمّد الواجهة. يحلّ RecyclerView هذه المشكلة بالحفاظ على مجموعة صغيرة من كائنات الواجهة — أكثر بقليل من عدد العناصر المرئية على الشاشة — وإعادة تدويرها مع تمرير المستخدم. عندما تختفي صف من أعلى الشاشة، يُفصل عنصره عن النافذة ويُعاد إلى المجموعة. حين يحتاج صف جديد للظهور في الأسفل، يُسترجع عنصر من المجموعة وتُكتب بياناته الجديدة عليه ثم يُربط بالنافذة.

الفكرة الجوهرية: لا يُنشئ RecyclerView من كائنات الواجهة أكثر مما يحتاجه لملء الشاشة. قد يحتوي مجموعة البيانات على مليون عنصر؛ بصمة الذاكرة لواجهة القائمة تظل ثابتة.

المعمارية ذات الثلاثة أجزاء

صُمِّم RecyclerView عن قصد ليكون مقسَّمًا إلى ثلاثة كائنات متعاونة، لكل منها مسؤولية واحدة:

  • RecyclerView — هو ViewGroup الذي يوضع في ملف تصميم XML. يدير مجموعة العناصر ويعالج أحداث اللمس ويُنسّق التمرير.
  • LayoutManager — يقرر أين توضع العناصر. يمنحك LinearLayoutManager قائمة رأسية أو أفقية، وGridLayoutManager شبكة، وStaggeredGridLayoutManager صفوفًا غير متساوية بأسلوب Pinterest.
  • Adapter — يربط بياناتك بمجموعة العناصر. يُنشئ حاملات عرض جديدة حين تكون المجموعة فارغة، ويربط بيانات جديدة بحاملة عرض معادة الاستخدام حين يحتاجها النظام.

إضافة RecyclerView إلى التصميم

أضف أولاً الاعتماد إلى build.gradle (وحدة التطبيق). في المشاريع الحديثة يصل عادةً عبر appcompat أو material، لكن يمكنك تصريحه مباشرةً:

// build.gradle (app) dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.2' }

ثم ضعه في تصميم النشاط:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:padding="8dp" /> </LinearLayout>

تعريف تصميم العنصر

كل صف يحتاج ملف تصميم خاص به. أنشئ res/layout/item_contact.xml:

<?xml version="1.0" encoding="utf-8"?> <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"> <TextView android:id="@+id/tvName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/tvPhone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#666666" /> </LinearLayout>

فئة النموذج

كائن Java بسيط يحمل بيانات صف واحد:

public class Contact { private final String name; private final String phone; public Contact(String name, String phone) { this.name = name; this.phone = phone; } public String getName() { return name; } public String getPhone() { return phone; } }

بناء المحوّل وحامل العرض

هنا يقع العمل الحقيقي. للـAdapter ثلاث مسؤوليات تُعبَّر عنها بثلاث دوال يجب تجاوزها:

  • onCreateViewHolder — ينفّخ تصميم العنصر ويلفّه في ViewHolder. يُستدعى فقط حين لا تحتوي المجموعة على حاملة معادة الاستخدام.
  • onBindViewHolder — يأخذ حاملة معادة الاستخدام (أو حديثة الإنشاء) ويملؤها ببيانات موضع معيّن. يُستدعى في كل مرة يظهر فيها صف على الشاشة.
  • getItemCount — يُخبر RecyclerView بعدد العناصر الموجودة.

الـViewHolder هو فئة داخلية تُخزّن مؤقتًا مراجع View لصف واحد. بدونها، سيضطر كل استدعاء لـonBindViewHolder إلى تنفيذ findViewById على العنصر الجذر — وهو اجتياز شجرة مكلف — عند كل حدث تمرير.

import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> { private final List<Contact> contacts; public ContactAdapter(List<Contact> contacts) { this.contacts = contacts; } // 1. نفّخ تصميم الصف وأنشئ ViewHolder @NonNull @Override public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_contact, parent, false); return new ContactViewHolder(itemView); } // 2. اربط البيانات بحامل العرض المُعاد @Override public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) { Contact contact = contacts.get(position); holder.tvName.setText(contact.getName()); holder.tvPhone.setText(contact.getPhone()); } // 3. أبلغ عن إجمالي عدد العناصر @Override public int getItemCount() { return contacts.size(); } // يُخزّن ViewHolder مراجع الواجهة الفرعية لصف واحد static class ContactViewHolder extends RecyclerView.ViewHolder { final TextView tvName; final TextView tvPhone; ContactViewHolder(@NonNull View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tvName); tvPhone = itemView.findViewById(R.id.tvPhone); } } }
أبقِ onBindViewHolder سريعًا. يعمل على خيط الواجهة ويُستدعى لكل صف يدخل نطاق الرؤية أثناء التمرير. لا استدعاءات شبكة، ولا قراءة قرص، ولا حسابات معقدة هنا — فقط قراءة بيانات واستدعاءات من نوع setText وsetImageBitmap. أي شيء أثقل من ذلك يجب أن يُنفَّذ في خيط خلفي قبل هذه النقطة.

ربط كل شيء معًا في النشاط

import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class ContactListActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact_list); // بناء بيانات تجريبية List<Contact> contacts = new ArrayList<>(); contacts.add(new Contact("Alice Martin", "+1 555-0101")); contacts.add(new Contact("Bob Chen", "+1 555-0202")); contacts.add(new Contact("Sara Ali", "+1 555-0303")); // توصيل RecyclerView RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new ContactAdapter(contacts)); } }

ثلاثة أسطر تقوم بكل العمل: تعيين LayoutManager (قائمة رأسية افتراضيًا)، وإنشاء المحوّل بالبيانات، وتعيينه. سيسأل RecyclerView فورًا المحوّل عن عدد العناصر ويبدأ في نفخ العناصر وربطها حسب الحاجة.

تحديث القائمة بكفاءة

استدعاء notifyDataSetChanged() يُجبر على إعادة ربط كل الصفوف المرئية — يعمل، لكنه يتخطى تحريكات تغيير العناصر ومُسرف. افضّل دوال الإشعار المحدَّدة حين تعلم ما الذي تغيّر بالضبط:

// أُدرج صف في الموضع 2 adapter.notifyItemInserted(2); // العنصر في الموضع 5 تحدّث adapter.notifyItemChanged(5); // العناصر من الموضع 1 حتى 3 حُذفت adapter.notifyItemRangeRemoved(1, 3);
لا تُعدّل قائمة البيانات من خيط خلفي ثم تستدعي دوال الإشعار على الخيط الرئيسي بدون مزامنة. يقرأ المحوّل القائمة في onBindViewHolder على خيط الواجهة. الكتابة المتزامنة من خيط آخر ستسبب تعطلاً أو تُنتج بيانات بصرية قديمة. نفّذ دائمًا تعديلات القائمة على الخيط الرئيسي، أو استخدم DiffUtil (يُغطّى في الدرس القادم) الذي يُسلّم عملية المبادلة النهائية إلى الخيط الرئيسي بأمان.

معالجة النقر على العناصر

على عكس ListView، لا يمتلك RecyclerView مستمع نقر مدمجًا. النمط الاصطلاحي هو تمرير واجهة رد نداء إلى المحوّل:

public interface OnContactClickListener { void onContactClick(Contact contact); } // في مُنشئ ContactAdapter: public ContactAdapter(List<Contact> contacts, OnContactClickListener listener) { this.contacts = contacts; this.listener = listener; } // في onBindViewHolder: holder.itemView.setOnClickListener(v -> listener.onContactClick(contacts.get(holder.getAdapterPosition())));

مرّر lambda من النشاط: new ContactAdapter(contacts, contact -> openDetail(contact)). هذا يُبقي المحوّل خاليًا من أي معرفة بالتنقل أو منطق الأعمال.

الخلاصة

يُبنى RecyclerView على ثلاثة كائنات تعمل معًا: المكوّن نفسه، وLayoutManager الذي يضع العناصر، وAdapter الذي يُنشئها ويربطها. نمط ViewHolder إلزامي — يُخزّن عمليات البحث findViewById كي يبقى onBindViewHolder سريعًا أثناء التمرير. هيكلة معالجة النقر كواجهة رد نداء تُبقي المحوّل مُركّزًا وقابلاً للاختبار. في الدرس القادم ستستبدل القائمة الثابتة ببيانات حية مجلوبة من قاعدة بيانات أو شبكة، وستتعلّم كيف يحسب DiffUtil أدنى التحديثات تلقائيًا.