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.
При виклику асинхронної функції замикання зберігається в купі разом із захопленими змінними. Коли операція завершується, система GCD або OperationQueue поміщає callback у відповідну чергу (головну або фонову). Після виконання 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
Swift-замикання — це самодостатній блок коду, який може бути переданий та використаний в іншій функції. Замикання бувають глобальними (іменованими), вкладеними та виразного рівня. @escaping позначає замикання, яке буде виконано після повернення з функції — це обов'язкова вимога для асинхронних callback. Без @escaping замикання може бути виконано тільки всередині тіла функції. Trailing closure syntax дозволяє передавати замикання після круглих дужок: 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 — ситуація, коли два об'єкти утримують сильні посилання один на одного, перешкоджаючи їх звільненню менеджером пам'яті. У 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 передає слухача в singleton-компонент (наприклад, EventBus або Service). WeakReference дозволяє збирачу сміття звільнити 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 (також відомий як Pyramid of Doom) — це ситуація, при якій безліч вкладених 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 надає cold-потоки з операторами 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. Недоліки: бойлерплейт-код, слабке посилання на delegate обов'язкове (weak var delegate), складність при захопленні контексту.
Часті запитання
Callback — це окремий випадок функції вищого порядку. Функція вищого порядку приймає іншу функцію як аргумент або повертає її. Callback — це функція, що передається спеціально для асинхронного виконання після завершення операції. Всі callback реалізуються через функції вищого порядку, але не кожна функція вищого порядку є callback.
За домовленістю callback має викликатися рівно один раз — або success, або failure. Множинний виклик одного callback вважається помилкою проектування. Для множинних подій (прогрес, потік даних) використовуйте Observable, Publisher або Flow — вони підтримують множинну емісію значень. Деякі API порушують це правило, що призводить до важковідшукуваних багів.
Trailing closure — синтаксичний цукор Swift, що дозволяє передати замикання після круглих дужок виклику функції. Якщо функція приймає замикання останнім аргументом, його можна винести за дужки: fetchData { result in ... }. Для кількох замикань trailing closure застосовується тільки до останнього, решта іменуються всередині дужок. Це покращує читабельність callback-орієнтованих API.
Використовуйте WeakReference для довгоживучих слухачів, скасовуйте корутини через Job.cancel() в onDestroy(), застосовуйте lifecycleScope для автоматичного скасування. ViewModel + LiveData/Flow вирішують проблему на архітектурному рівні. Уникайте передачі Activity-контексту в статичні callback — використовуйте Application context. 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також