Callback — що це таке, функції зворотного виклику та як вони працюють

Автор: IT Sectr Опубліковано: 2026-03-17 Час читання: 11 хв

Callback — це функція, яка передається в іншу функцію як аргумент і виконується після завершення асинхронної операції. У мобільній розробці callback застосовується для обробки результатів мережевих запитів, роботи з базами даних та анімацій. За даними Apple Documentation (2025), замикання в Swift є основною формою callback і використовуються в URLSession, GCD та Combine. В Android callback реалізовано через інтерфейси, лямбди Kotlin та ListenableFuture.

Головне

  • Callback — функція зворотного виклику, що передається як аргумент для асинхронного виконання.
  • Swift використовує замикання (closures) з ключовим словом @escaping для callback.
  • Kotlin застосовує лямбда-вирази та функції вищого порядку для callback.
  • Retain cycle — витік пам'яті при захопленні self у callback на iOS.
  • Callback Hell — проблема вкладених callback, що вирішується через async/await та корутини.

Що таке Callback?

Callback (функція зворотного виклику) — це виконуваний код, який передається в іншу функцію та викликається після завершення певної дії. У мобільній розробці callback є фундаментальним механізмом асинхронного програмування, що дозволяє реагувати на завершення мережевих запитів, таймерів, анімацій та операцій введення-виведення без блокування основного потоку. Swift та Kotlin надають вбудовані синтаксичні конструкції для створення callback — замикання та лямбди відповідно.

Принцип роботи 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 використовуються для сортування (компаратори) та обходу колекцій. Асинхронні callback застосовуються для мережевих запитів, читання файлів та роботи з сенсорами. Різниця критична для розуміння threading: синхронний callback виконується в тому ж потоці, асинхронний — у потоці, визначеному диспетчером (DispatchQueue в iOS, Dispatchers в Kotlin).

Як працює Callback в iOS та Android?

Механізм callback на обох платформах заснований на одному принципі: функція передається як об'єкт першого класу та зберігається до моменту виконання. Однак реалізації відрізняються через різні мовні парадигми. У iOS callback — це замикання (closure), яке захоплює змінні з навколишнього контексту. У Android callback найчастіше реалізовано через анонімні класи або лямбда-вирази Kotlin, що компілюються в FunctionalInterface.

Життєвий цикл Callback в iOS

При виклику асинхронної функції замикання зберігається в купі разом із захопленими змінними. Коли операція завершується, система GCD або OperationQueue поміщає callback у відповідну чергу (головну або фонову). Після виконання callback видаляється з пам'яті при відсутності сильних посилань. Capture list ([weak self]) запобігає утриманню об'єкта після його деалокації. Без capture list виникає retain cycle, при якому об'єкт і callback посилаються один на одного.

swift
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)
    }
}

Життєвий цикл Callback в Android

В Android callback передається через інтерфейс або лямбду. При виконанні асинхронної операції через ExecutorService або корутину, callback зберігається в пам'яті до завершення фонової роботи. Kotlin-лямбди компілюються в анонімні класи, що захоплюють зовнішні змінні. Відсутність слабких посилань у JVM вимагає ручного керування: обнулення callback в onDestroy() або скасування корутин через Job.cancel(). ViewModel та LiveData вирішують цю проблему на рівні архітектурного компонента.

kotlin
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 та Kotlin

Синтаксис callback визначається мовними можливостями роботи з функціями як з об'єктами першого класу. У Swift замикання мають короткий синтаксис з автоматичними іменами аргументів ($0, $1). У Kotlin лямбди також підтримують it для єдиного аргументу. Відмінності проявляються в обробці захоплення змінних (capture list у Swift проти змінних посилань у Kotlin) та типізації (Result vs Result).

Callback у Swift: замикання (closures)

Swift-замикання — це самодостатній блок коду, який може бути переданий та використаний в іншій функції. Замикання бувають глобальними (іменованими), вкладеними та виразного рівня. @escaping позначає замикання, яке буде виконано після повернення з функції — це обов'язкова вимога для асинхронних callback. Без @escaping замикання може бути виконано тільки всередині тіла функції. Trailing closure syntax дозволяє передавати замикання після круглих дужок: fetchData { result in ... }.

swift
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)
    }
}

Callback у Kotlin: лямбди та функції вищого порядку

Kotlin підтримує функції вищого порядку, що приймають інші функції як параметри. Callback у Kotlin передається через параметр типу (T) -> Unit або (T) -> R для повернення значення. suspend-функції корутин Kotlin замінюють callback на послідовний код, але callback залишається в Java-сумісних API та Android SDK (View.setOnClickListener, TextWatcher). Kotlin-лямбди автоматично захоплюють val-змінні, var-змінні потребують обгорток змінності.

kotlin
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 cycles та витоки пам'яті в Callback

Retain cycle — ситуація, коли два об'єкти утримують сильні посилання один на одного, перешкоджаючи їх звільненню менеджером пам'яті. У Swift retain cycle виникає, коли viewController захоплює замикання, а замикання захоплює self. У Kotlin/Java витік відбувається, коли Activity передає внутрішній клас або лямбду в тривалу фонову операцію. За даними WWDC Session 10216 (2024), неправильне керування замиканнями — третя за частотою причина витоків пам'яті в iOS-додатках.

Retain cycles у Swift

Swift використовує Automatic Reference Counting (ARC), який звільняє об'єкт при обнуленні лічильника посилань. Capture list [weak self] або [unowned self] у замиканні запобігає retain cycle. weak self створює опціональне посилання, яке стає nil при деалокації об'єкта. unowned self передбачає, що об'єкт живе довше за замикання — при порушенні цього припущення відбувається crash. Рекомендується використовувати weak self як безпечний варіант за замовчуванням.

swift
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

В Android callback-витік відбувається, коли Activity або Fragment передає слухача в singleton-компонент (наприклад, EventBus або Service). WeakReference дозволяє збирачу сміття звільнити Activity, навіть якщо на неї є слабке посилання. Lifecycle-aware компоненти (LiveData, Flow) вирішують проблему автоматично. Kotlin-лямбди, що захоплюють Activity-контекст, також можуть викликати витік: лямбда неявно зберігає посилання на this.

kotlin
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 Hell (також відомий як Pyramid of Doom) — це ситуація, при якій безліч вкладених callback створюють глибоко вкладену структуру коду, складну для читання та налагодження. Кожен наступний крок вимагає очікування завершення попереднього, що призводить до вкладеності 5-10 рівнів. Ця проблема характерна для послідовних асинхронних операцій: завантаження даних → парсинг → збереження в БД → оновлення UI.

Рішення в Swift: async/await

Swift 5.5 ввів асинхронні функції (async/await), які дозволяють писати асинхронний код послідовно. AsyncSequence та AsyncStream замінюють callback-орієнтовані ітерації. Combine фреймворк надає оператори flatMap, merge, combineLatest для композиції асинхронних потоків без вкладеності. Однак callback залишається необхідним для роботи з Objective-C API та сторонніми бібліотеками без async-підтримки.

swift
// Вкладені 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: корутини та Flow

Kotlin корутини замінюють callback на suspend-функції з послідовним виконанням. Flow надає cold-потоки з операторами map, flatMapConcat, combine. CoroutineScope дозволяє скасовувати всі запущені корутини при знищенні компонента. Room, Retrofit та інші бібліотеки Jetpack мають вбудовану підтримку suspend-функцій, усуваючи необхідність у callback для стандартних операцій.

kotlin
// Послідовні 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 vs Delegate: що обрати?

Callback та Delegate — два підходи до асинхронного сповіщення, і вибір між ними залежить від архітектурних вимог. Callback підходить для разових операцій з одним результатом. Delegate призначений для множинних подій з різними сигнатурами методів. Apple рекомендує delegate для складних протоколів з кількома методами, callback — для простих замикань з одним результатом. В Android callback замінює delegate в більшості випадків через підтримку лямбд.

Коли обирати Callback

Callback оптимальний для операцій з єдиним результатом: мережевий запит, читання файлу, анімація з completion-блоком. Переваги: компактний синтаксис, відсутність окремого протоколу, пряме захоплення контексту. Недоліки: складність при множинних результатах (прогрес, пауза, скасування), неможливість множинної відправки (якщо callback може бути викликано більше одного разу — використовуйте publisher).

Коли обирати Delegate

Delegate підходить для протоколів з кількома обов'язковими та опціональними методами: UITableViewDelegate, CLLocationManagerDelegate, Bluetooth-з'єднання. Переваги: чітка типізація кожного методу, документація через протокол, підтримка опціональних методів через @objc optional. Недоліки: бойлерплейт-код, слабке посилання на delegate обов'язкове (weak var delegate), складність при захопленні контексту.

Часті запитання

У чому різниця між callback та функцією вищого порядку?

Callback — це окремий випадок функції вищого порядку. Функція вищого порядку приймає іншу функцію як аргумент або повертає її. Callback — це функція, що передається спеціально для асинхронного виконання після завершення операції. Всі callback реалізуються через функції вищого порядку, але не кожна функція вищого порядку є callback.

Чи може callback бути викликано кілька разів?

За домовленістю callback має викликатися рівно один раз — або success, або failure. Множинний виклик одного callback вважається помилкою проектування. Для множинних подій (прогрес, потік даних) використовуйте Observable, Publisher або Flow — вони підтримують множинну емісію значень. Деякі API порушують це правило, що призводить до важковідшукуваних багів.

Що таке trailing closure у Swift?

Trailing closure — синтаксичний цукор Swift, що дозволяє передати замикання після круглих дужок виклику функції. Якщо функція приймає замикання останнім аргументом, його можна винести за дужки: fetchData { result in ... }. Для кількох замикань trailing closure застосовується тільки до останнього, решта іменуються всередині дужок. Це покращує читабельність callback-орієнтованих API.

Як уникнути витоку пам'яті при callback в Android?

Використовуйте WeakReference для довгоживучих слухачів, скасовуйте корутини через Job.cancel() в onDestroy(), застосовуйте lifecycleScope для автоматичного скасування. ViewModel + LiveData/Flow вирішують проблему на архітектурному рівні. Уникайте передачі Activity-контексту в статичні callback — використовуйте Application context. Kotlin-лямбди захоплюють this неявно, перевіряйте через memory profiler.

Чи замінять async/await callback повністю?

Async/await замінює callback для послідовного асинхронного коду, але не для event-driven архітектури. Callback залишається в системних API (View.OnClickListener, URLSession делегати), зворотних викликах з прогресом та сторонніх бібліотеках. Повна заміна неможлива через зворотну сумісність. Сучасна стратегія — використовувати async/await з callback-обгортками (continuation у Swift, suspendCancellableCoroutine у Kotlin).

Підсумки

  • Callback — функція зворотного виклику, що передається як аргумент для асинхронного виконання після завершення операції.
  • Swift реалізує callback через замикання з @escaping, capture list [weak self] та trailing closure syntax.
  • Kotlin використовує лямбди, функції вищого порядку та suspend-функції корутин для асинхронності.
  • Retain cycle в iOS запобігається capture list; в Android — WeakReference та Lifecycle-aware компонентами.
  • Callback Hell вирішується через async/await у Swift та корутини з Flow у Kotlin.
  • Delegate кращий за callback для протоколів з кількома методами; callback — для разових операцій.
  • Використовуйте callback для простих асинхронних операцій, async/await для послідовних ланцюжків, delegate для множинних подій.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також