Callback — е функция, която се предава на друга функция като аргумент и се изпълнява след завършване на асинхронна операция. В мобилното разработване callback се използва за обработка на резултати от мрежови заявки, работа с бази данни и анимации. Според Apple Documentation (2025), затварянията в Swift са основната форма на callback и се използват в URLSession, GCD и Combine. В Android callback се реализира чрез интерфейси, ламбда изрази на Kotlin и ListenableFuture.
Основни точки
Callback (функция за обратно извикване) — е изпълним код, който се предава на друга функция и се извиква след завършване на определено действие. В мобилното разработване callback е основният механизъм на асинхронното програмиране, позволяващ реагиране на завършване на мрежови заявки, таймери, анимации и входно-изходни операции без блокиране на основната нишка. Swift и Kotlin предоставят вградени синтактични конструкции за създаване на callback — съответно затваряния и ламбди.
Функция от по-висок ред приема друга функция като параметър и я извиква след изпълнение на основната си логика. Потокът на управление се връща към извикващия чрез callback, откъдето идва и името. В iOS callback се прилага в UIKit (анимации UIView.animate), Foundation (URLSession.dataTask) и Combine (sink). В Android callback се използва в View.OnClickListener, Retrofit Callback и Room DAO. Съвременните API все по-често заменят callback с async/await или корутини, но разбирането на callback е необходимо за работа с наследен код и нисконивови API.
Callback може да бъде синхронен (извиква се веднага във функцията) и асинхронен (извиква се по-късно от друга нишка или опашка). Синхронните callback се използват за сортиране (компаратори) и обхождане на колекции. Асинхронни callback се прилагат за мрежови заявки, четене на файлове и работа със сензори. Разликата е критична за разбиране на threading: синхронният callback се изпълнява в същата нишка, асинхронният — в нишката, определена от диспечера (DispatchQueue в iOS, Dispatchers в Kotlin).
Механизмът на callback на двете платформи се основава на същия принцип: функцията се предава като обект от първи клас и се съхранява до момента на изпълнение. Реализациите обаче се различават поради различни езикови парадигми. В iOS callback е затваряне (closure), което улавя променливи от околния контекст. В Android callback най-често се реализира чрез анонимни класове или ламбда изрази на Kotlin, компилирани във FunctionalInterface.
При извикване на асинхронна функция, затварянето се съхранява в heap заедно с уловените променливи. Когато операцията завърши, системата GCD или OperationQueue поставя callback в съответната опашка (main queue или background queue). След изпълнение, callback се премахва от паметта при липса на силни референции. Capture list ([weak self]) предотвратява задържането на обекта след неговата деалокация. Без 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 се предава чрез интерфейс или ламбда. При изпълнение на асинхронна операция чрез ExecutorService или корутина, callback се съхранява в паметта до завършване на фоновата работа. Ламбдите на Kotlin се компилират в анонимни класове, които улавят външни променливи. Липсата на слаби референции в 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) }
}
}
}
}
// Използване с ламбда
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 ламбдите също поддържат it за единичен аргумент. Разликите се проявяват в обработката на улавяне на променливи (capture list в Swift срещу променливи референции в Kotlin) и типизация (Result<Success, Failure> срещу Result<T>).
Swift затваряне е самодостатъчен блок код, който може да бъде предаден и използван в друга функция. Затварянията могат да бъдат глобални (именувани), вложени и expression-level. @escaping маркира затваряне, което ще бъде изпълнено след връщане от функцията — това е задължително изискване за асинхронни callback. Без @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 корутини заместват callback с последователен код, но callback остава в Java-съвместими API и Android SDK (View.setOnClickListener, TextWatcher). Ламбдите на Kotlin автоматично улавят val променливи, var променливи изискват променливи обвивки.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Пример с ламбда
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — ситуация, при която два обекта поддържат силни референции един към друг, предотвратявайки освобождаването им от garbage collector. В Swift retain cycle възниква, когато viewController улови затваряне, а затварянето улови self. В Kotlin/Java изтичане възниква, когато Activity предаде вътрешен клас или ламбда на дълготрайна фонова операция. Според WWDC Session 10216 (2024), неправилното управление на затваряния е третата най-честа причина за изтичане на памет в iOS приложения.
Swift използва Automatic Reference Counting (ARC), който освобождава обекта при нулиране на брояча на референции. Capture list [weak self] или [unowned self] в затварянето предотвратява retain cycle. 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 предаде слушател на сингълтън компонент (напр. EventBus или Service). WeakReference позволява на garbage collector да освободи Activity, дори ако има слаба референция към нея. Lifecycle-aware компоненти (LiveData, Flow) решават проблема автоматично. Ламбдите на Kotlin, които улавят контекст на Activity, също могат да причинят изтичане: ламбдата имплицитно съхранява референция към 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 обаче остава необходим за работа с Objective-C API и библиотеки на трети страни без 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)
}
Kotlin корутините заместват callback със suspend функции с последователно изпълнение. Flow предоставя студени потоци с оператори map, flatMapConcat, combine. CoroutineScope позволява отмяна на всички стартирани корутини при унищожаване на компонента. 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()
}
}
}
}
// Корутини — решение
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 в повечето случаи поради поддръжка на ламбди.
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, която позволява предаване на затваряне след кръглите скоби на извикване на функция. Ако функцията приема затваряне като последен аргумент, то може да бъде поставено извън скобите: fetchData { result in ... }. За множество затваряния trailing closure се прилага само към последното, останалите се именуват вътре в скобите. Това подобрява четимостта на API, базирани на callback.
Използвайте WeakReference за дългоживущи слушатели, отменяйте корутини чрез Job.cancel() в onDestroy(), прилагайте lifecycleScope за автоматично отменяне. ViewModel + LiveData/Flow решават проблема на архитектурно ниво. Избягвайте предаване на контекст на Activity в статични callback — използвайте контекст на приложението. Ламбдите на Kotlin имплицитно улавят this, проверявайте чрез memory profiler.
Async/await заменя callback за последователен асинхронен код, но не и за event-driven архитектура. Callback остава в системни API (View.OnClickListener, делегати на URLSession), обратни извиквания с напредък и библиотеки на трети страни. Пълната замяна е невъзможна поради обратна съвместимост. Съвременната стратегия е използване на async/await с callback обвивки (continuation в Swift, suspendCancellableCoroutine в Kotlin).
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също