ربط البيانات بالقوائم
في الدرس السابق بنيت الهيكل الأساسي: RecyclerView في تخطيطك، وفئة ViewHolder، ومحوّل RecyclerView.Adapter بسيط. الآن يبدأ العمل الحقيقي — تزويد مجموعة بيانات حية، وتوصيلها بالمحوّل، وفهم متى بالضبط يستدعي Android كودك ولماذا. يركّز هذا الدرس كليًا على عقد المحوّل والأنماط التي يطبّقها المطوّر المحترف للحفاظ على تزامن واجهة المستخدم مع البيانات.
مراجعة سريعة: ما الذي يفعله المحوّل فعلًا
فكّر في RecyclerView.Adapter باعتباره الجسر بين قائمة Java عادية والصفوف التي تراها على الشاشة. إنه يجيب عن ثلاثة أسئلة يطرحها RecyclerView باستمرار:
- كم عدد العناصر؟ —
getItemCount()
- أنشئ حامل عرض لهذا النوع من العروض. —
onCreateViewHolder()
- اربط البيانات في الموضع N بهذا الحامل. —
onBindViewHolder()
كل منطق ربط البيانات يعيش في onBindViewHolder(). كل شيء آخر مجرد سباكة.
فئة نموذج ملموسة
المحوّلات الجيدة تعمل على كائنات نموذج مكتوبة بأنواع محددة، لا على سلاسل نصية خام أو خرائط. عرّف نموذج Java بسيطًا للعناصر في قائمتك:
// Product.java
public class Product {
private final String name;
private final String category;
private final double price;
public Product(String name, String category, double price) {
this.name = name;
this.category = category;
this.price = price;
}
public String getName() { return name; }
public String getCategory() { return category; }
public double getPrice() { return price; }
}
الحقول غير القابلة للتغيير مع getters فقط هي الافتراض الآمن لعناصر القائمة — لا يمكن تعديلها عن طريق الخطأ بينما يقرأها المحوّل.
محوّل ProductAdapter كامل
فيما يلي محوّل كامل لـ RecyclerView يعرض قائمة من كائنات Product. اقرأ التعليقات المضمّنة بعناية — كل سطر يقابل قاعدة من قواعد المنصة.
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.ArrayList;
import java.util.List;
import java.util.Locale;
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
// المحوّل يملك نسخة من البيانات، وليس مرجعًا لقائمة المُستدعي.
private final List<Product> items;
public ProductAdapter(List<Product> products) {
// نسخة دفاعية: تعزل المحوّل عن التعديلات الخارجية.
this.items = new ArrayList<>(products);
}
// --- التضمين ---
@NonNull
@Override
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// يجب أن يستخدم LayoutInflater سياق الوالد وألا يرفق فورًا.
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_product, parent, false);
return new ProductViewHolder(itemView);
}
// --- الربط ---
@Override
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
Product product = items.get(position);
holder.nameView.setText(product.getName());
holder.categoryView.setText(product.getCategory());
// تنسيق السعر بمنزلتين عشريتين ورمز العملة.
holder.priceView.setText(String.format(Locale.getDefault(), "$%.2f", product.getPrice()));
}
// --- العدد ---
@Override
public int getItemCount() {
return items.size();
}
// --- ViewHolder ---
static class ProductViewHolder extends RecyclerView.ViewHolder {
final TextView nameView;
final TextView categoryView;
final TextView priceView;
ProductViewHolder(@NonNull View itemView) {
super(itemView);
// يُستدعى findViewById مرة واحدة هنا، وليس في كل عملية ربط.
nameView = itemView.findViewById(R.id.tv_product_name);
categoryView = itemView.findViewById(R.id.tv_product_category);
priceView = itemView.findViewById(R.id.tv_product_price);
}
}
}
لماذا نضمّن مع attachToRoot = false؟ تمرير false كوسيط ثالث لـ inflate() يخبر المُضمِّن باستخدام الوالد فقط لسياق معاملات التخطيط، لا لإرفاق العرض فورًا. يُدير RecyclerView الإرفاق بنفسه. إذا مررت true، يُرفق العرض مرتين وسترى تعطّلًا أو تخطيطًا مشوّهًا أثناء التشغيل.
تخطيط العنصر المقابل
ملف التخطيط res/layout/item_product.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="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tv_product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_product_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_product_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
توصيل المحوّل بـ RecyclerView في نشاطك
في Activity الخاص بك (أو Fragment)، أنشئ البيانات وأنشئ المحوّل وأرفقه مرة واحدة:
// ProductListActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import java.util.Arrays;
import java.util.List;
public class ProductListActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_product_list);
List<Product> products = Arrays.asList(
new Product("Laptop Pro", "Electronics", 1299.99),
new Product("Desk Chair", "Furniture", 349.00),
new Product("Notebook", "Stationery", 4.50),
new Product("Headphones", "Electronics", 199.99)
);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new ProductAdapter(products));
}
}
تحديث القائمة: notifyDataSetChanged مقابل الإشعارات المحدّدة
عندما تتغير بياناتك يجب إخبار المحوّل. الأسلوب السهل — adapter.notifyDataSetChanged() — يعمل لكنه يُعيد رسم كل صف مرئي حتى لو تغيّر عنصر واحد فقط. يوفّر Android بدائل دقيقة:
notifyItemInserted(int position) — يُطلق حركة الإدراج لصف واحد.
notifyItemRemoved(int position) — يُطلق حركة الحذف.
notifyItemChanged(int position) — يُعيد رسم صف واحد بدون حركة.
notifyItemRangeInserted(int positionStart, int itemCount) — إدراج مجموعة بكفاءة.
النمط المعتاد هو كشف تابع على المحوّل يُعدّل قائمته الداخلية ويُطلق الإشعار الصحيح:
// داخل ProductAdapter — أضف للفئة
public void addProduct(Product product) {
items.add(product);
notifyItemInserted(items.size() - 1);
}
public void removeProduct(int position) {
items.remove(position);
notifyItemRemoved(position);
}
public void replaceAll(List<Product> newProducts) {
items.clear();
items.addAll(newProducts);
notifyDataSetChanged(); // استبدال كامل — الإشعار المحدّد غير عملي هنا
}
افضّل الإشعارات المحدّدة للحصول على حركات سلسة. يُجبر notifyDataSetChanged() على إعادة رسم كاملة ويُلغي حركات تغيير العنصر الافتراضية. استخدمه فقط عند استبدال مجموعة البيانات بالكامل. بالنسبة للتغييرات التدريجية (إضافة صف واحد، حذف صف واحد)، تبدو التوابع المحدّدة أفضل بكثير للمستخدم وهي أيضًا أكثر كفاءة.
تحسين المعرّفات الثابتة
إذا كان لكل عنصر في مجموعة بياناتك معرّف فريد دائم (مفتاح أساسي لقاعدة بيانات مثلًا)، قم بتفعيل المعرّفات الثابتة. هذا يتيح لـ RecyclerView تتبع العناصر عبر تغييرات مجموعة البيانات وتطبيق الحركات الصحيحة حتى بعد notifyDataSetChanged():
public ProductAdapter(List<Product> products) {
this.items = new ArrayList<>(products);
setHasStableIds(true); // استدعِه في المُنشئ
}
@Override
public long getItemId(int position) {
return items.get(position).getId(); // أعد المعرّف الفريد من قاعدة البيانات
}
لا تستدعِ setHasStableIds(true) دون تجاوز getItemId(). التنفيذ الافتراضي يُعيد RecyclerView.NO_ID (أي -1) لكل عنصر، لذا لا يستطيع RecyclerView التمييز بين الصفوف، وستكون الحركات خاطئة أو قابلة للتعطّل. إذا اخترت المعرّفات الثابتة، يجب تقديم قيمة فريدة حقيقية من نموذجك.
ماذا عن DiffUtil؟
DiffUtil هو الحل الاحترافي لحساب الحد الأدنى من التغييرات بين قائمتين وإرسال الإشعارات المحدّدة الدقيقة تلقائيًا. يعتمد على خوارزمية Myers للفروقات ويعمل على خيط في الخلفية. ستُصادفه في قواعد الكود الحقيقية، لكنه يبني مباشرةً على APIs الإشعارات التي تعلّمتها للتو — أتقنها أولًا. يتناول درس مستقبلي في هذه الدورة DiffUtil وListAdapter بعمق.
الخلاصة
تغذية البيانات في RecyclerView تتلخّص في ثلاثة أجزاء متحرّكة: فئة نموذج مكتوبة بنوع محدّد، ومحوّل ينفّذ الردود الثلاث المطلوبة، ومدير تخطيط يُخبر RecyclerView بكيفية ترتيب الصفوف. كل منطق الربط يعيش في onBindViewHolder()؛ اجعله سريعًا — لا استدعاءات قاعدة بيانات، ولا حسابات ثقيلة. عند تغيّر البيانات، أطلق أكثر إشعار محدّد ممكن. مع هذه الأساسيات في مكانها، أنت مستعد لإضافة معالجة نقر العناصر وتخطيطات صفوف أغنى في الدروس القادمة.