Adapter هو نمط تصميم هيكلي يحول واجهة فئة إلى واجهة أخرى يتوقعها العميل. في تطوير التطبيقات المحمولة، يُستخدم Adapter بشكل أساسي في RecyclerView.Adapter لنظام Android وUITableViewDataSource لنظام iOS. وفقًا لـ Google I/O (2024)، يُستخدم RecyclerView في 95% من تطبيقات Android لعرض القوائم، وكل تطبيق يتطلب تنفيذ Adapter الخاص به.
أهم النقاط
Adapter هو نمط هيكلي يحل مشكلة عدم توافق الواجهات. يغلف كائنًا (Adaptee) في فئة (Adapter) بواجهة يتوقعها العميل (Target). يعمل العميل مع Target دون معرفة بوجود Adaptee. هذا نوع من نمط Wrapper.
// الفئة الموجودة بواجهة غير متوافقة
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// الواجهة الهدف (ما يتوقعه العميل)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — يحول LegacyAuthApi إلى AuthService
class LegacyAuthAdapter(
private val legacyApi: LegacyAuthApi
) : AuthService {
override suspend fun login(credentials: Credentials): Result {
val token = "${credentials.login}:${credentials.password}"
.encodeToByteArray().let { Base64.encodeToString(it) }
val response = legacyApi.loginWithToken(token)
return if (response["status"] == "ok") {
Result.success(User(response["user_id"] as Int))
} else {
Result.failure(AuthException("Login failed"))
}
}
}LegacyAuthAdapter يحول استدعاء login(credentials) إلى loginWithToken(token) لواجهة API القديمة. يعمل العميل (ViewModel) من خلال واجهة AuthService ولا يعرف ما إذا كان تحتها أحدث Firebase Auth أو واجهة قديمة من عشر سنوات. هذا يسمح باستبدال التطبيقات دون تغيير كود العميل.
RecyclerView.Adapter هو التطبيق الأكثر شيوعًا لنمط Adapter في Android. يحول البيانات (قائمة كائنات) إلى ViewHolders التي يعرضها RecyclerView على الشاشة. مع ظهور ListAdapter (Android Architecture Components)، حصل النمط على دعم مدمج لحساب الفروقات (diff) لتحريك التغييرات.
class UserAdapter(
private val onItemClick: (User) -> Unit
) : ListAdapter (DiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(
holder: UserViewHolder,
position: Int
) {
holder.bind(getItem(position), onItemClick)
}
class UserViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val tvName = itemView.findViewById(R.id.tvName)
fun bind(user: User, click: (User) -> Unit) {
tvName.text = user.name
itemView.setOnClickListener { click(user) }
}
}
private class DiffCallback : DiffUtil.ItemCallback () {
override fun areItemsTheSame(old: User, new: User) = old.id == new.id
override fun areContentsTheSame(old: User, new: User) = old == new
}
} UserAdapter يستخدم ListAdapter مع DiffUtil لإعادة رسم القائمة بكفاءة. عندما يتم تحديث البيانات، يحسب DiffCallback الفرق بين القائمة القديمة والجديدة، ويقوم RecyclerView بتحريك العناصر المتغيرة فقط — دون إعادة رسم القائمة بأكملها. هذه ميزة رئيسية مقارنة بـ ListView القديم الذي كان يتطلب notifyDataSetChanged() يدويًا.
UITableViewDataSource هو في نفس الوقت نمط Adapter ونمط DataSource. يحول بيانات النموذج إلى خلايا جدول. منذ iOS 13، قدمت Apple DiffableDataSource — بديل حديث، مثل ListAdapter في Android، يحسب التغييرات تلقائيًا ويحرك التحديثات.
class UserTableViewAdapter: NSObject, UITableViewDataSource {
var items: [User] = []
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
let user = items[indexPath.row]
cell.textLabel?.text = user.name
return cell
}
}
// النسخة الحديثة: DiffableDataSource
enum Section { case main }
class ModernUserAdapter {
var dataSource: UITableViewDiffableDataSource<Section, User>!
func configure(for tableView: UITableView) {
dataSource = UITableViewDiffableDataSource(
tableView: tableView
) { tableView, indexPath, user in
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
cell.textLabel?.text = user.name
return cell
}
}
func update(with users: [User]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
snapshot.appendSections([.main])
snapshot.appendItems(users)
dataSource.apply(snapshot, animatingDifferences: true)
}
}ModernUserAdapter مع DiffableDataSource يقوم تلقائيًا بتحريك التغييرات عند تحديث البيانات: تظهر العناصر أو تختفي أو تتحرك مع رسوم متحركة سلسة. DiffableDataSource في iOS وListAdapter في Android يحلان نفس المشكلة بواجهات برمجة مختلفة ولكن بنفس المنطق — حساب الفروقات + الرسوم المتحركة التلقائية.
Object Adapter يستخدم التركيب: المحول يحتوي على مرجع لكائن Adaptee ويفوض الاستدعاءات إليه. Class Adapter يستخدم الوراثة: يرث المحول من Adaptee وينفذ الواجهة الهدف في نفس الوقت. في Java وSwift وKotlin، Class Adapter غير ممكن بسبب عدم وجود وراثة متعددة — يبقى فقط Object Adapter.
| الخاصية | Object Adapter | Class Adapter |
|---|---|---|
| الآلية | التركيب (يحتوي على Adaptee) | الوراثة (يمدد Adaptee) |
| المرونة | أعلى — يمكن تغيير Adaptee في وقت التشغيل | أقل — Adaptee ثابت في وقت الترجمة |
| الاقتران | ضعيف (عبر واجهة) | قوي (عبر الوراثة) |
| تكييف الفئات الفرعية | نعم — Adaptee يمكن أن يكون أي فئة فرعية | لا — فقط فئة أصل محددة |
| التوفر | Java, Kotlin, Swift, C++, C# | لغات ذات وراثة متعددة فقط (C++) |
في تطوير التطبيقات المحمولة، يُستخدم Object Adapter فقط. RecyclerView.Adapter يحتوي على مراجع للبيانات وLayoutInflater، UITableViewDataSource يحتوي على مصفوفة عناصر. التركيب يجعل الكود أكثر مرونة وقابلية للاختبار مقارنة بالوراثة.
Adapter لا يُستخدم فقط للقوائم. دعنا ننظر في ثلاثة سيناريوهات من ممارسة تطوير التطبيقات المحمولة حيث يحل النمط مشكلة دمج المكونات غير المتوافقة.
| السيناريو | Adaptee | Target | Adapter |
|---|---|---|---|
| دمج مكتبة مصادقة قديمة | LegacyAuthLib (قائم على الاستدعاء) | AuthService (suspend) | LegacyAuthAdapter |
| تكييف JSON مع نموذج جديد | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| واجهة موحدة لخدمات الدفع | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
كل Adapter يغلف منطق التحويل: GsonAdapter يترجم سلسلة JSON إلى كائن عبر @SerializedName، PushServiceAdapter يجرد تسجيل الرمز المميز لمنصات مختلفة. يعمل العميل (منطق الأعمال) مع واجهة موحدة ولا يعتمد على التطبيق المحدد.
الأخطاء في تنفيذ Adapter تؤدي إلى بطء القوائم وتسرب الذاكرة وأخطاء في تحديث البيانات. دعنا نستعرض ثلاث مشاكل شائعة.
يتم استدعاء onBindViewHolder لكل خلية أثناء التمرير. إنشاء كائنات أو تحليل JSON أو استدعاء قاعدة البيانات في هذه الطريقة يؤدي إلى انخفاض الإطارات (frame drops). الحل: إجراء جميع الحسابات الثقيلة مسبقًا، وتمرير البيانات الجاهزة إلى المحول. استخدام مكتبات التخزين المؤقت للصور — Glide، Coil، Kingfisher.
استدعاء notifyDataSetChanged() في كل تحديث يعيد رسم القائمة بأكملها، مما يسبب وميضًا وفقدان التركيز على الإدخالات. الحل: استخدام ListAdapter مع DiffUtil في Android أو DiffableDataSource في iOS. حساب الفروقات يستغرق <1 مللي ثانية للقوائم حتى 1000 عنصر ويضمن رسومًا متحركة سلسة.
المحول يعيش بنفس مدة RecyclerView/UITableView. تخزين Bitmap أو مصفوفات كبيرة في المحول يؤدي إلى تسرب الذاكرة عند تغيير التكوين (تدوير الشاشة، تغيير السمة). الحل: يجب أن يخزن المحول فقط نماذج بيانات خفيفة (data class / struct)، وتحميل الموارد الثقيلة عبر ViewHolder عند الطلب.
الأسئلة الشائعة
Adapter يحول واجهة Adaptee إلى واجهة Target — يحصل العميل على طريقة جديدة للتفاعل. Proxy يوفر نفس واجهة الكائن الأصلي، لكنه يضيف التحكم في الوصول أو التخزين المؤقت أو التهيئة البطيئة. Proxy لا يغير الواجهة، Adapter يغيرها.
RecyclerView.Adapter يحول البيانات (مثل قائمة User) إلى ViewHolder يمكن لـ RecyclerView عرضه. RecyclerView يتوقع ViewHolder، البيانات بتنسيق List<User> — يقوم Adapter بتكييف أحدهما مع الآخر. بالإضافة إلى ذلك، يدير Adapter دورة حياة ViewHolder من خلال تجمع إعادة التدوير، مما يحسن أداء القائمة.
Object Adapter يستخدم التركيب: يحتوي على مرجع لـ Adaptee ويفوض الاستدعاءات إليه. Class Adapter يستخدم الوراثة المتعددة: يرث من Adaptee وينفذ Target. في Java وKotlin وSwift، يتوفر فقط Object Adapter بسبب عدم وجود وراثة متعددة للفئات. Class Adapter ممكن فقط في C++.
Adapter يُستخدم عندما تحتاج إلى جعل فئتين موجودتين تعملان معًا عن طريق تحويل واجهة إحداهما. Facade يُستخدم عندما تحتاج إلى تبسيط التفاعل مع نظام فرعي معقد من خلال توفير واجهة بسيطة. Adapter للتوافق، Facade للتبسيط.
نعم، باستخدام الإغلاقات. بدلاً من واجهة بطريقة واحدة، يمكن تمرير دالة. على سبيل المثال، في Swift: let adapter = { (data: Data) -> [CellConfig] in /* تحويل */ }. يُسمى هذا النهج المحول الوظيفي. ومع ذلك، للطرق 2+، يبقى البروتوكول/الواجهة مفضلًا — فهو يجعل العقد صريحًا.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.