Callback هي دالة تُمرر إلى دالة أخرى كوسيطة ويتم تنفيذها بعد اكتمال عملية غير متزامنة. في تطوير التطبيقات المحمولة، يُستخدم callback لمعالجة نتائج طلبات الشبكة، والعمل مع قواعد البيانات والرسوم المتحركة. وفقًا لوثائق Apple (2025)، فإن الإغلاقات (closures) في Swift هي الشكل الأساسي للـ callback وتُستخدم في URLSession وGCD وCombine. في Android، يتم تنفيذ callback من خلال الواجهات، وتعبيرات Kotlin lambda وListenableFuture.
النقاط الرئيسية
Callback (دالة الاستدعاء العكسي) هو كود قابل للتنفيذ يُمرر إلى دالة أخرى ويُستدعى بعد اكتمال إجراء معين. في تطوير التطبيقات المحمولة، يُعد callback آلية أساسية للبرمجة غير المتزامنة، مما يسمح بالاستجابة لاكتمال طلبات الشبكة والمؤقتات والرسوم المتحركة وعمليات الإدخال/الإخراج دون حظر الخيط الرئيسي. يوفر Swift وKotlin تراكيب نحوية مدمجة لإنشاء callback — الإغلاقات والتعبيرات lambda على التوالي.
تقبل دالة الترتيب الأعلى دالة أخرى كمعامل وتستدعيها بعد تنفيذ منطقها الرئيسي. يتم إعادة تدفق التحكم إلى المتصل عبر callback، ومن هنا جاء الاسم. في iOS، يُستخدم callback في UIKit (رسوم UIView.animate المتحركة)، وFoundation (URLSession.dataTask)، وCombine (sink). في Android، يُستخدم callback في View.OnClickListener وRetrofit Callback وRoom DAO. تستبدل واجهات برمجة التطبيقات الحديثة بشكل متزايد callback بـ async/await أو الكوروتينات، لكن فهم callback ضروري للعمل مع الكود القديم وواجهات برمجة التطبيقات منخفضة المستوى.
يمكن أن يكون callback متزامنًا (يُستدعى فورًا داخل الدالة) أو غير متزامن (يُستدعى لاحقًا من خيط أو قائمة انتظار أخرى). تُستخدم الـ callbacks المتزامنة للترتيب (المقارنات) واجتياز المجموعات. تُستخدم الـ callbacks غير المتزامنة لطلبات الشبكة وقراءة الملفات والعمل مع أجهزة الاستشعار. الفرق مهم جدًا لفهم threading: يتم تنفيذ الـ callback المتزامن في نفس الخيط، بينما يتم تنفيذ الـ callback غير المتزامن في خيط تحدده وحدة التوزيع (DispatchQueue في iOS، Dispatchers في Kotlin).
آلية الـ callback في كلتا المنصتين تقوم على نفس المبدأ: تمرر دالة ككائن من الدرجة الأولى وتُخزن حتى لحظة التنفيذ. لكن التنفيذ يختلف بسبب اختلاف نماذج اللغات. في iOS، الـ callback هو إغلاق (closure) يلتقط المتغيرات من السياق المحيط. في Android، يتم تنفيذ الـ callback غالبًا من خلال الفئات المجهولة أو تعبيرات Kotlin lambda، التي تُترجم إلى FunctionalInterface.
عند استدعاء دالة غير متزامنة، يتم تخزين الإغلاق في heap مع المتغيرات الملتقطة. عند اكتمال العملية، يضع نظام GCD أو OperationQueue الـ callback في قائمة الانتظار المناسبة (قائمة الانتظار الرئيسية أو الخلفية). بعد التنفيذ، يُحذف الـ callback من الذاكرة عند عدم وجود مراجع قوية. تمنع قائمة الالتقاط ([weak self]) الاحتفاظ بالكائن بعد تحريره. بدون قائمة الالتقاط، ينشأ retain cycle حيث يشير الكائن والـ callback إلى بعضهما البعض.
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
completion(.success(data))
}
task.resume()
}
// الاستخدام مع [weak self]
fetchData { [weak self] result in
guard let self else { return }
switch result {
case .success(let data):
self.updateUI(data)
case .failure(let error):
self.showError(error)
}
}
في Android، يُمرر الـ callback من خلال واجهة أو lambda. عند تنفيذ عملية غير متزامنة عبر ExecutorService أو كوروتين، يُخزن الـ callback في الذاكرة حتى اكتمال العمل في الخلفية. تُترجم تعبيرات Kotlin lambda إلى فئات مجهولة تلتقط المتغيرات الخارجية. يتطلب غياب المراجع الضعيفة في JVM إدارة يدوية: إلغاء الـ callback في onDestroy() أو إلغاء الكوروتينات عبر Job.cancel(). يحل ViewModel وLiveData هذه المشكلة على مستوى المكون المعماري.
interface Callback<T> {
fun onSuccess(data: T)
fun onError(error: Throwable)
}
class Repository {
fun loadData(callback: Callback<List<User>>) {
thread {
try {
val result = api.fetchUsers()
runOnUiThread { callback.onSuccess(result) }
} catch (e: Exception) {
runOnUiThread { callback.onError(e) }
}
}
}
}
// الاستخدام مع lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
يتم تحديد صياغة الـ callback من خلال قدرات اللغة على التعامل مع الدوال ككائنات من الدرجة الأولى. في Swift، تتميز الإغلاقات بصياغة موجزة مع أسماء وسيطات تلقائية ($0, $1). في Kotlin، تدعم تعبيرات lambda أيضًا it لوسيطة واحدة. تظهر الاختلافات في معالجة التقاط المتغيرات (قائمة الالتقاط في Swift مقابل المراجع القابلة للتغيير في Kotlin) والتصنيف (Result
إغلاق Swift هو كتلة كود مستقلة يمكن تمريرها واستخدامها في دالة أخرى. يمكن أن تكون الإغلاقات عامة (مسماة) ومتداخلة وعلى مستوى التعبير. @escaping تميز الإغلاق الذي سيتم تنفيذه بعد عودة الدالة — وهذا شرط إلزامي للـ callbacks غير المتزامنة. بدون @escaping، يمكن تنفيذ الإغلاق فقط داخل جسم الدالة. تسمح صياغة trailing closure بتمرير الإغلاق بعد الأقواس: fetchData { result in ... }.
typealias NetworkResult = (Result<[String: Any], Error>) -> Void
func performRequest(
url: URL,
then handler: @escaping NetworkResult
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
handler(Result {
guard let json = try JSONSerialization.jsonObject(with: data)
else { throw NetworkError.invalidData }
return json as! [String: Any]
})
}
task.resume()
}
performRequest(url: url) { result in
switch result {
case .success(let json): process(json)
case .failure(let error): log(error.localizedDescription)
}
}
يدعم Kotlin دوال الترتيب الأعلى التي تقبل دوال أخرى كمعاملات. يُمرر callback في Kotlin عبر معامل من النوع (T) -> Unit أو (T) -> R لقيم الإرجاع. تستبدل دوال suspend في كوروتينات Kotlin الـ callbacks بكود تسلسلي، لكن تبقى الـ callbacks في واجهات برمجة التطبيقات المتوافقة مع Java وAndroid SDK (View.setOnClickListener، TextWatcher). تلتقط تعبيرات Kotlin lambda تلقائيًا متغيرات val، بينما تتطلب متغيرات var أغلفة قابلية التغيير.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// مثال مع lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle هو حالة يحتفظ فيها كائنان بمراجع قوية لبعضهما البعض، مما يمنع مدير الذاكرة من تحريرهما. في Swift، ينشأ retain cycle عندما يلتقط viewController إغلاقًا ويلتقط الإغلاق self. في Kotlin/Java، يحدث تسرب عندما تمرر Activity فئة داخلية أو lambda إلى عملية خلفية طويلة الأمد. وفقًا لجلسة WWDC 10216 (2024)، فإن الإدارة غير الصحيحة للإغلاقات هي ثالث أكثر أسباب تسرب الذاكرة شيوعًا في تطبيقات iOS.
يستخدم Swift العد التلقائي للمراجع (ARC) الذي يحرر الكائن عندما يصل عداد المراجع إلى الصفر. تمنع قائمة الالتقاط [weak self] أو [unowned self] في الإغلاق retain cycles. ينشئ weak self مرجعًا اختياريًا يصبح nil عند تحرير الكائن. يفترض unowned self أن الكائن يعيش أطول من الإغلاق — عند انتهاك هذا الافتراض يحدث crash. يُوصى باستخدام weak self كخيار آمن افتراضي.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// تم التصحيح: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
في Android، يحدث تسرب الذاكرة بسبب callback عندما تمرر Activity أو Fragment مستمعًا إلى مكون singleton (مثل EventBus أو Service). يسمح WeakReference لمجمع القمامة بتحرير Activity حتى إذا كان هناك مرجع ضعيف إليها. تحل المكونات lifecycle-aware (LiveData، Flow) المشكلة تلقائيًا. يمكن أن تسبب تعبيرات Kotlin lambda التي تلتقط سياق Activity تسريبات أيضًا: تخزن lambda ضمنيًا مرجعًا إلى this.
class SafeCallbackManager {
private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()
fun addListener(callback: (String) -> Unit) {
listeners.add(WeakReference(callback))
}
fun notifyAll(data: String) {
val iterator = listeners.iterator()
while (iterator.hasNext()) {
val ref = iterator.next().get()
if (ref != null) ref(data)
else iterator.remove()
}
}
}
// الاستخدام في Fragment
manager.addListener { result ->
// WeakReference لا يحتفظ بـ Fragment
updateUI(result)
}
Callback Hell (المعروف أيضًا باسم هرم الموت) هو حالة تنشئ فيها العديد من الـ callbacks المتداخلة بنية كود عميقة التداخل، يصعب قراءتها وتصحيحها. تتطلب كل خطوة تالية انتظار اكتمال السابقة، مما يؤدي إلى تداخل من 5-10 مستويات. هذه المشكلة مميزة للعمليات غير المتزامنة المتسلسلة: تحميل البيانات → التحليل → الحفظ في قاعدة البيانات → تحديث واجهة المستخدم.
قدم Swift 5.5 الدوال غير المتزامنة (async/await)، التي تتيح كتابة كود غير متزامن بشكل تسلسلي. AsyncSequence وAsyncStream يستبدلان التكرارات القائمة على callback. يوفر إطار Combine عوامل مثل flatMap وmerge وcombineLatest لتكوين التدفقات غير المتزامنة دون تداخل. ومع ذلك، تبقى الـ callbacks ضرورية للعمل مع واجهات برمجة تطبيقات Objective-C والمكتبات الخارجية دون دعم async.
// Callbacks المتداخلة — Callback Hell
loginUser(credentials) { user in
fetchProfile(user.id) { profile in
downloadAvatar(profile.avatarUrl) { image in
cacheImage(image) { success in
updateUI(user, profile, image)
}
}
}
}
// async/await — الحل
func loadUserExperience() async throws {
let user = try await loginUser(credentials)
let profile = try await fetchProfile(user.id)
let image = try await downloadAvatar(profile.avatarUrl)
try await cacheImage(image)
updateUI(user, profile, image)
}
تستبدل كوروتينات Kotlin الـ callbacks بدوال suspend للتنفيذ التسلسلي. يوفر Flow تدفقات باردة مع عوامل مثل map وflatMapConcat وcombine. يسمح CoroutineScope بإلغاء جميع الكوروتينات الجارية عند تدمير المكون. تحتوي Room وRetrofit ومكتبات Jetpack الأخرى على دعم مدمج لدوال suspend، مما يلغي الحاجة إلى الـ callbacks في العمليات القياسية.
// Callbacks المتسلسلة — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// الكوروتينات — الحل
suspend fun loadUserData() {
val user = withContext(Dispatchers.IO) { api.login(credentials) }
val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
withContext(Dispatchers.IO) { file.save(bytes) }
textView.text = "Done"
}
Callback وDelegate هما نهجان للإعلام غير المتزامن، ويعتمد الاختيار على المتطلبات المعمارية. الـ Callback مناسب للعمليات لمرة واحدة بنتيجة واحدة. الـ Delegate مصمم للأحداث المتعددة بتوقيعات دوال مختلفة. توصي Apple باستخدام delegate للبروتوكولات المعقدة بعدة دوال، وcallback للإغلاقات البسيطة بنتيجة واحدة. في Android، يحل الـ callback محل delegate في معظم الحالات بسبب دعم lambda.
الـ Callback مثالي للعمليات بنتيجة واحدة: طلب شبكة، قراءة ملف، رسم متحرك مع كتلة إكمال. المزايا: صياغة مدمجة، عدم الحاجة لبروتوكول منفصل، التقاط مباشر للسياق. العيوب: تعقيد في النتائج المتعددة (تقدم، إيقاف مؤقت، إلغاء)، استحالة الإرسال المتعدد (إذا كان من الممكن استدعاء callback أكثر من مرة — استخدم publisher).
الـ Delegate مناسب للبروتوكولات بعدة دوال إلزامية واختيارية: UITableViewDelegate، CLLocationManagerDelegate، اتصالات Bluetooth. المزايا: تصنيف واضح لكل دالة، توثيق عبر البروتوكول، دعم الدوال الاختيارية عبر @objc optional. العيوب: كود نموذجي (boilerplate)، مرجع ضعيف إلى delegate إلزامي (weak var delegate)، تعقيد في التقاط السياق.
الأسئلة الشائعة
Callback هو حالة خاصة من دالة الترتيب الأعلى. تقبل دالة الترتيب الأعلى دالة أخرى كوسيطة أو تعيدها. الـ Callback هو دالة تُمرر خصيصًا للتنفيذ غير المتزامن بعد اكتمال العملية. يتم تنفيذ جميع الـ callbacks من خلال دوال الترتيب الأعلى، لكن ليست كل دالة ترتيب أعلى هي callback.
وفقًا للاتفاق، يجب استدعاء callback مرة واحدة بالضبط — إما success أو failure. يُعتبر الاستدعاء المتعدد لنفس callback خطأ في التصميم. للأحداث المتعددة (التقدم، تدفق البيانات)، استخدم Observable أو Publisher أو Flow — فهي تدعم الإصدار المتعدد للقيم. تخالف بعض واجهات برمجة التطبيقات هذه القاعدة، مما يؤدي إلى أخطاء يصعب اكتشافها.
Trailing closure هو سكر نحوي في Swift يسمح بتمرير إغلاق بعد الأقواس المستديرة لاستدعاء الدالة. إذا كانت الدالة تقبل إغلاقًا كآخر وسيطة، يمكن وضعه خارج الأقواس: fetchData { result in ... }. للإغلاقات المتعددة، يُطبق trailing closure على الأخير فقط؛ والباقي يُسمى داخل الأقواس. هذا يحسن قابلية قراءة واجهات برمجة التطبيقات القائمة على callback.
استخدم WeakReference للمستمعين طويلي العمر، وألغِ الكوروتينات عبر Job.cancel() في onDestroy()، واستخدم lifecycleScope للإلغاء التلقائي. يحل ViewModel + LiveData/Flow المشكلة على المستوى المعماري. تجنب تمرير سياق Activity إلى callbacks ثابتة — استخدم Application context. تلتقط تعبيرات Kotlin lambda this ضمنيًا، تحقق باستخدام memory profiler.
Async/await يحل محل الـ callbacks للكود غير المتزامن التسلسلي، لكن ليس للهندسة المعمارية القائمة على الأحداث. تبقى الـ callbacks في واجهات برمجة تطبيقات النظام (View.OnClickListener، مفوضو URLSession) والـ callbacks ذات التقدم والمكتبات الخارجية. الاستبدال الكامل مستحيل بسبب التوافق العكسي. الاستراتيجية الحديثة هي استخدام async/await مع أغلفة callback (continuation في Swift، suspendCancellableCoroutine في Kotlin).
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا