Callback — تابعی است که به عنوان آرگومان به تابع دیگری ارسال میشود و پس از تکمیل عملیات ناهمزمان اجرا میگردد. در توسعه موبایل از callback برای پردازش نتایج درخواستهای شبکه، کار با پایگاه داده و انیمیشنها استفاده میشود. طبق Apple Documentation (2025)، closures در Swift شکل اصلی callback هستند و در URLSession، GCD و Combine استفاده میشوند. در Android، callback از طریق interfaceها، lambdaهای Kotlin و ListenableFuture پیادهسازی میشود.
نکات اصلی
Callback (تابع بازگشتی) — کد قابل اجرایی است که به تابع دیگری ارسال میشود و پس از تکمیل یک عمل خاص فراخوانی میگردد. در توسعه موبایل، callback مکانیزم اساسی برنامهنویسی ناهمزمان است و امکان واکنش به تکمیل درخواستهای شبکه، تایمرها، انیمیشنها و عملیات ورودی/خروجی را بدون مسدود کردن نخ اصلی فراهم میکند. Swift و Kotlin ساختارهای نحوی داخلی برای ایجاد callback ارائه میدهند — به ترتیب closures و lambdaها.
تابع مرتبه بالا تابع دیگری را به عنوان پارامتر میپذیرد و پس از اجرای منطق اصلی خود آن را فراخوانی میکند. جریان کنترل از طریق callback به caller بازمیگردد، از این رو نام آن. در iOS، callback در UIKit (انیمیشنهای UIView.animate)، Foundation (URLSession.dataTask) و Combine (sink) استفاده میشود. در Android، callback در View.OnClickListener، Retrofit Callback و Room DAO استفاده میشود. APIهای مدرن به طور فزایندهای callback را با async/await یا coroutine جایگزین میکنند، اما درک callback برای کار با کدهای قدیمی و APIهای سطح پایین ضروری است.
Callback میتواند همزمان (بلافاصله در داخل تابع فراخوانی شود) و ناهمزمان (بعداً از نخ یا صف دیگر فراخوانی شود) باشد. callbackهای همزمان برای مرتبسازی (comparatorها) و پیمایش مجموعهها استفاده میشوند. callbackهای ناهمزمان برای درخواستهای شبکه، خواندن فایلها و کار با سنسورها استفاده میشوند. تفاوت برای درک threading حیاتی است: callback همزمان در همان نخ اجرا میشود، ناهمزمان — در نخی که توسط dispatcher تعیین میشود (DispatchQueue در iOS، Dispatchers در Kotlin).
مکانیزم callback در هر دو پلتفرم بر اساس یک اصل است: تابع به عنوان یک شی درجه یک ارسال میشود و تا زمان اجرا ذخیره میگردد. با این حال، پیادهسازیها به دلیل پارادایمهای زبانی متفاوت، تفاوت دارند. در iOS، callback یک closure است که متغیرها را از زمینه اطراف میگیرد. در Android، callback اغلب از طریق کلاسهای ناشناس یا عبارات lambda Kotlin که به FunctionalInterface کامپایل میشوند، پیادهسازی میگردد.
هنگام فراخوانی یک تابع ناهمزمان، closure همراه با متغیرهای گرفته شده در heap ذخیره میشود. وقتی عملیات کامل میشود، سیستم GCD یا OperationQueue callback را در صف مربوطه (main queue یا background queue) قرار میدهد. پس از اجرا، در صورت عدم وجود ارجاع قوی، callback از حافظه حذف میشود. Capture list ([weak self]) از نگهداری شی پس از deallocation جلوگیری میکند. بدون capture list، 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 از طریق interface یا lambda ارسال میشود. هنگام اجرای عملیات ناهمزمان از طریق ExecutorService یا coroutine، callback تا تکمیل کار پسزمینه در حافظه ذخیره میشود. lambdaهای Kotlin به کلاسهای ناشناسی کامپایل میشوند که متغیرهای خارجی را میگیرند. عدم وجود ارجاع ضعیف در JVM نیاز به مدیریت دستی دارد: صفر کردن callback در onDestroy() یا لغو coroutine از طریق 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، closures نحو مختصری با نامهای خودکار آرگومان ($0, $1) دارند. در Kotlin، lambdaها نیز از it برای آرگومان تکی پشتیبانی میکنند. تفاوتها در پردازش گرفتن متغیرها (capture list در Swift در مقابل ارجاعات تغییرپذیر در Kotlin) و تایپبندی (Result<Success, Failure> در مقابل Result<T>) ظاهر میشود.
Closure در Swift یک بلوک کد خودکفا است که میتواند به تابع دیگری ارسال و استفاده شود. Closures میتوانند سراسری (نامدار)، تو در تو و expression-level باشند. @escaping closureای را مشخص میکند که پس از بازگشت از تابع اجرا خواهد شد — این یک الزام اجباری برای callbackهای ناهمزمان است. بدون @escaping، closure فقط میتواند در داخل بدنه تابع اجرا شود. Trailing closure syntax اجازه میدهد 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 در coroutineهای Kotlin جایگزین callback با کد ترتیبی میشوند، اما callback در APIهای سازگار با Java و Android SDK (View.setOnClickListener, TextWatcher) باقی میماند. lambdaهای Kotlin به طور خودکار متغیرهای val را میگیرند، متغیرهای var نیاز به wrapperهای تغییرپذیر دارند.
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 یک closure را میگیرد و closure self را میگیرد. در Kotlin/Java، نشت زمانی رخ میدهد که Activity یک کلاس داخلی یا lambda را به یک عملیات طولانی پسزمینه ارسال میکند. طبق WWDC Session 10216 (2024)، مدیریت نادرست closures سومین علت شایع نشت حافظه در برنامههای iOS است.
Swift از Automatic Reference Counting (ARC) استفاده میکند که شی را هنگام صفر شدن شمارنده ارجاع آزاد میکند. Capture list [weak self] یا [unowned self] در closure از retain cycle جلوگیری میکند. weak self یک ارجاع اختیاری ایجاد میکند که هنگام deallocation شی nil میشود. unowned self فرض میکند که شی بیشتر از closure عمر میکند — نقض این فرض باعث 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 یک listener را به یک کامپوننت singleton (مثلاً EventBus یا Service) ارسال میکند. WeakReference به garbage collector اجازه میدهد Activity را حتی با وجود ارجاع ضعیف آزاد کند. کامپوننتهای Lifecycle-aware (LiveData, Flow) مشکل را به طور خودکار حل میکنند. lambdaهای Kotlin که زمینه 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 (همچنین به عنوان هرم نابودی شناخته میشود) — وضعیتی است که در آن تعداد زیادی callback تو در تو یک ساختار کد عمیقاً تودرتو ایجاد میکنند که خواندن و اشکالزدایی آن دشوار است. هر مرحله بعدی نیاز به انتظار برای تکمیل مرحله قبلی دارد که منجر به تودرتو شدن 5-10 سطح میشود. این مشکل برای عملیات ناهمزمان ترتیبی مشخص است: بارگذاری داده → تجزیه → ذخیره در پایگاه داده → بهروزرسانی UI.
Swift 5.5 توابع ناهمزمان (async/await) را معرفی کرد که امکان نوشتن کد ناهمزمان به صورت ترتیبی را فراهم میکنند. AsyncSequence و AsyncStream جایگزین تکرارهای مبتنی بر callback میشوند. فریمورک Combine عملگرهای flatMap، merge، combineLatest را برای ترکیب جریانهای ناهمزمان بدون تودرتو ارائه میدهد. با این حال، callback برای کار با APIهای Objective-C و کتابخانههای شخص ثالث بدون پشتیبانی async ضروری باقی میماند.
// callbackهای تو در تو — 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)
}
Coroutineهای Kotlin جایگزین callback با توابع suspend با اجرای ترتیبی میشوند. Flow جریانهای cold با عملگرهای map، flatMapConcat، combine ارائه میدهد. CoroutineScope امکان لغو تمام coroutineهای راهاندازی شده را هنگام نابودی کامپوننت فراهم میکند. Room، Retrofit و سایر کتابخانههای Jetpack دارای پشتیبانی داخلی از توابع suspend هستند و نیاز به callback را برای عملیات استاندارد از بین میبرند.
// callbackهای ترتیبی — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Coroutineها — راهحل
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 و برای closures ساده با یک نتیجه از callback استفاده کنید. در Android، callback به دلیل پشتیبانی از lambda در بیشتر موارد جایگزین delegate میشود.
Callback برای عملیات با یک نتیجه واحد بهینه است: درخواست شبکه، خواندن فایل، انیمیشن با بلوک completion. مزایا: نحو فشرده، عدم وجود پروتکل جداگانه، گرفتن مستقیم زمینه. معایب: پیچیدگی در نتایج متعدد (پیشرفت، توقف، لغو)، عدم امکان ارسال چندگانه (اگر callback ممکن است بیش از یک بار فراخوانی شود — از publisher استفاده کنید).
Delegate برای پروتکلهایی با چندین متد اجباری و اختیاری مناسب است: UITableViewDelegate، CLLocationManagerDelegate، اتصالات Bluetooth. مزایا: تایپبندی واضح هر متد، مستندات از طریق پروتکل، پشتیبانی از متدهای اختیاری از طریق @objc optional. معایب: کد boilerplate، ارجاع ضعیف اجباری به delegate (weak var delegate)، پیچیدگی در گرفتن زمینه.
سوالات متداول
Callback یک مورد خاص از تابع مرتبه بالا است. تابع مرتبه بالا تابع دیگری را به عنوان آرگومان میپذیرد یا آن را برمیگرداند. Callback تابعی است که به طور خاص برای اجرای ناهمزمان پس از تکمیل عملیات ارسال میشود. همه callbackها از طریق توابع مرتبه بالا پیادهسازی میشوند، اما هر تابع مرتبه بالا callback نیست.
طبق قرارداد، callback باید دقیقاً یک بار فراخوانی شود — یا success یا failure. فراخوانی چندگانه یک callback خطای طراحی محسوب میشود. برای رویدادهای متعدد (پیشرفت، جریان داده) از Observable، Publisher یا Flow استفاده کنید — آنها از انتشار چندگانه مقدار پشتیبانی میکنند. برخی APIها این قانون را نقض میکنند که منجر به باگهای سختیاب میشود.
Trailing closure — شکر نحوی Swift است که اجازه میدهد closure پس از پرانتزهای گرد فراخوانی تابع ارسال شود. اگر تابع closure را به عنوان آخرین آرگومان بپذیرد، میتوان آن را خارج از پرانتز قرار داد: fetchData { result in ... }. برای چندین closure، trailing closure فقط برای آخرین مورد اعمال میشود، بقیه در داخل پرانتز نامگذاری میشوند. این کار خوانایی APIهای مبتنی بر callback را بهبود میبخشد.
برای listenerهای طولانیعمر از WeakReference استفاده کنید، coroutineها را از طریق Job.cancel() در onDestroy() لغو کنید، برای لغو خودکار از lifecycleScope استفاده کنید. ViewModel + LiveData/Flow مشکل را در سطح معماری حل میکنند. از ارسال زمینه Activity به callbackهای ایستا خودداری کنید — از Application context استفاده کنید. lambdaهای Kotlin به طور ضمنی this را میگیرند، با memory profiler بررسی کنید.
Async/await جایگزین callback برای کد ناهمزمان ترتیبی میشود، اما نه برای معماری event-driven. Callback در APIهای سیستمی (View.OnClickListener، delegateهای URLSession)، فراخوانیهای با پیشرفت و کتابخانههای شخص ثالث باقی میماند. جایگزینی کامل به دلیل سازگاری معکوس غیرممکن است. استراتژی مدرن استفاده از async/await با wrapperهای callback است (continuation در Swift، suspendCancellableCoroutine در Kotlin).
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید