Callback — co to jest, funkcje zwrotne i jak działają

Autor: IT Sectr Opublikowano: 2026-03-17 Czas czytania: 11 min

Callback — to funkcja przekazywana do innej funkcji jako argument i wykonywana po zakończeniu operacji asynchronicznej. W programowaniu mobilnym callback jest używany do obsługi wyników zapytań sieciowych, pracy z bazami danych i animacji. Według Apple Documentation (2025), domknięcia w Swift są podstawową formą callback i są używane w URLSession, GCD i Combine. W Androidzie callback jest zaimplementowany przez interfejsy, lambdy Kotlin i ListenableFuture.

Najważniejsze

  • Callback — funkcja zwrotna przekazywana jako argument do wykonania asynchronicznego.
  • Swift używa domknięć (closures) ze słowem kluczowym @escaping dla callback.
  • Kotlin stosuje wyrażenia lambda i funkcje wyższego rzędu dla callback.
  • Retain cycle — wyciek pamięci przy przechwytywaniu self w callback na iOS.
  • Callback Hell — problem zagnieżdżonych callback, rozwiązywany przez async/await i korutyny.

Czym jest Callback?

Callback (funkcja zwrotna) — to wykonywalny kod przekazywany do innej funkcji i wywoływany po zakończeniu określonego działania. W programowaniu mobilnym callback jest podstawowym mechanizmem programowania asynchronicznego, pozwalającym reagować na zakończenie zapytań sieciowych, timerów, animacji i operacji wejścia-wyjścia bez blokowania głównego wątku. Swift i Kotlin udostępniają wbudowane konstrukcje składniowe do tworzenia callback — odpowiednio domknięcia i lambdy.

Zasada działania Callback

Funkcja wyższego rzędu przyjmuje inną funkcję jako parametr i wywołuje ją po wykonaniu swojej głównej logiki. Przepływ sterowania wraca do wywołującego przez callback, stąd nazwa. W iOS callback jest stosowany w UIKit (animacje UIView.animate), Foundation (URLSession.dataTask) i Combine (sink). W Android callback jest używany w View.OnClickListener, Retrofit Callback i Room DAO. Nowoczesne API coraz częściej zastępują callback przez async/await lub korutyny, ale znajomość callback jest niezbędna do pracy z legacy kodem i niskopoziomowymi API.

Synchroniczny i asynchroniczny Callback

Callback może być synchroniczny (wywoływany natychmiast wewnątrz funkcji) i asynchroniczny (wywoływany później z innego wątku lub kolejki). Synchroniczne callback są używane do sortowania (komparatory) i przeglądania kolekcji. Asynchroniczne callback są stosowane do zapytań sieciowych, odczytu plików i pracy z czujnikami. Różnica jest kluczowa dla zrozumienia wielowątkowości: synchroniczny callback wykonuje się w tym samym wątku, asynchroniczny — w wątku określonym przez dyspozytora (DispatchQueue w iOS, Dispatchers w Kotlin).

Jak działa Callback w iOS i Android?

Mechanizm callback na obu platformach opiera się na tej samej zasadzie: funkcja jest przekazywana jako obiekt pierwszej klasy i przechowywana do momentu wykonania. Jednak implementacje różnią się ze względu na różne paradygmaty językowe. W iOS callback to domknięcie (closure), które przechwytuje zmienne z otaczającego kontekstu. W Android callback najczęściej jest realizowany przez anonimowe klasy lub wyrażenia lambda Kotlin, kompilowane do FunctionalInterface.

Cykl życia Callback w iOS

Przy wywołaniu funkcji asynchronicznej domknięcie jest zapisywane na stercie wraz z przechwyconymi zmiennymi. Po zakończeniu operacji system GCD lub OperationQueue umieszcza callback w odpowiedniej kolejce (main queue lub background queue). Po wykonaniu callback jest usuwany z pamięci przy braku silnych referencji. Capture list ([weak self]) zapobiega utrzymywaniu obiektu po jego dealokacji. Bez capture list powstaje retain cycle, w którym obiekt i callback wzajemnie się przechowują.

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

// Użycie z [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)
    }
}

Cykl życia Callback w Android

W Androidzie callback jest przekazywany przez interfejs lub lambdę. Podczas wykonywania operacji asynchronicznej przez ExecutorService lub korutynę, callback jest przechowywany w pamięci do zakończenia pracy w tle. Lambdy Kotlin są kompilowane do anonimowych klas przechwytujących zmienne zewnętrzne. Brak słabych referencji w JVM wymaga ręcznego zarządzania: wyzerowanie callback w onDestroy() lub anulowanie korutyn przez Job.cancel(). ViewModel i LiveData rozwiązują ten problem na poziomie komponentu architektonicznego.

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

// Użycie z lambdą
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

Składnia Callback w Swift i Kotlin

Składnia callback zależy od możliwości językowych pracy z funkcjami jako obiektami pierwszej klasy. W Swift domknięcia mają zwięzłą składnię z automatycznymi nazwami argumentów ($0, $1). W Kotlin lambdy również obsługują it dla pojedynczego argumentu. Różnice przejawiają się w obsłudze przechwytywania zmiennych (capture list w Swift vs zmienne mutowalne w Kotlin) i typizacji (Result<Success, Failure> vs Result<T>).

Callback w Swift: domknięcia (closures)

Domknięcie w Swift to samowystarczalny blok kodu, który może być przekazany i użyty w innej funkcji. Domknięcia mogą być globalne (nazwane), zagnieżdżone i expression-level. @escaping oznacza domknięcie, które zostanie wykonane po powrocie z funkcji — to obowiązkowy wymóg dla asynchronicznych callback. Bez @escaping domknięcie może być wykonane tylko wewnątrz ciała funkcji. Trailing closure syntax pozwala przekazywać domknięcie za nawiasami okrągłymi: 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 w Kotlin: lambdy i funkcje wyższego rzędu

Kotlin obsługuje funkcje wyższego rzędu, przyjmujące inne funkcje jako parametry. Callback w Kotlin jest przekazywany przez parametr typu (T) -> Unit lub (T) -> R dla zwracanej wartości. suspend-funkcje korutyn Kotlin zastępują callback kodem sekwencyjnym, ale callback pozostaje w API zgodnych z Javą i Android SDK (View.setOnClickListener, TextWatcher). Lambdy Kotlin automatycznie przechwytują zmienne val, zmienne var wymagają opakowań mutowalnych.

kotlin
fun <T, R> processWithCallback(
    input: T,
    transform: (T) -> R,
    onResult: (R) -> Unit
) {
    thread {
        val result = transform(input)
        runOnUiThread { onResult(result) }
    }
}

// Przykład z lambdą
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cycles i wycieki pamięci w Callback

Retain cycle — sytuacja, w której dwa obiekty utrzymują silne referencje wzajemnie, uniemożliwiając ich zwolnienie przez garbage collector. W Swift retain cycle powstaje, gdy viewController przechwytuje domknięcie, a domknięcie przechwytuje self. W Kotlin/Java wyciek następuje, gdy Activity przekazuje wewnętrzną klasę lub lambdę do długotrwałej operacji w tle. Według WWDC Session 10216 (2024), nieprawidłowe zarządzanie domknięciami jest trzecią najczęstszą przyczyną wycieków pamięci w aplikacjach iOS.

Retain cycles w Swift

Swift używa Automatic Reference Counting (ARC), który zwalnia obiekt przy wyzerowaniu licznika referencji. Capture list [weak self] lub [unowned self] w domknięciu zapobiega retain cycle. weak self tworzy opcjonalną referencję, która staje się nil przy dealokacji obiektu. unowned self zakłada, że obiekt żyje dłużej niż domknięcie — naruszenie tego założenia powoduje crash. Zaleca się używanie weak self jako bezpiecznej opcji domyślnej.

swift
class DataController {
    var onDataUpdate: ((String) -> Void)?

    func setupCallback() {
        // Retain cycle!
        onDataUpdate = { text in
            self.process(text)
        }

        // Naprawiono: [weak self]
        onDataUpdate = { [weak self] text in
            guard let self else { return }
            self.process(text)
        }
    }

    func process(_ input: String) { }
}

Wycieki pamięci w Android

W Androidzie wyciek callback występuje, gdy Activity lub Fragment przekazuje słuchacza do komponentu singleton (np. EventBus lub Service). WeakReference pozwala garbage collectorowi zwolnić Activity, nawet jeśli istnieje do niego słaba referencja. Komponenty Lifecycle-aware (LiveData, Flow) rozwiązują problem automatycznie. Lambdy Kotlin przechwytujące kontekst Activity również mogą powodować wyciek: lambda niejawnie przechowuje referencję do 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()
        }
    }
}

// Użycie we Fragment
manager.addListener { result ->
    // WeakReference nie przechowuje Fragment
    updateUI(result)
}

Callback Hell i sposoby radzenia sobie

Callback Hell (znany również jako Piramida Zagłady) — to sytuacja, w której wiele zagnieżdżonych callback tworzy głęboko zagnieżdżoną strukturę kodu, trudną do czytania i debugowania. Każdy kolejny krok wymaga oczekiwania na zakończenie poprzedniego, co prowadzi do zagnieżdżenia 5-10 poziomów. Problem ten jest charakterystyczny dla sekwencyjnych operacji asynchronicznych: pobranie danych → parsowanie → zapis do bazy → aktualizacja UI.

Rozwiązania w Swift: async/await

Swift 5.5 wprowadził funkcje asynchroniczne (async/await), które pozwalają pisać kod asynchroniczny sekwencyjnie. AsyncSequence i AsyncStream zastępują iteracje oparte na callback. Framework Combine udostępnia operatory flatMap, merge, combineLatest do kompozycji strumieni asynchronicznych bez zagnieżdżania. Jednak callback pozostaje niezbędny do pracy z API Objective-C i bibliotekami innych firm bez wsparcia async.

swift
// Zagnieżdżone 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 — rozwiązanie
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)
}

Rozwiązania w Kotlin: korutyny i Flow

Korutyny Kotlin zastępują callback funkcjami suspend z sekwencyjnym wykonaniem. Flow udostępnia strumienie cold z operatorami map, flatMapConcat, combine. CoroutineScope pozwala anulować wszystkie uruchomione korutyny przy zniszczeniu komponentu. Room, Retrofit i inne biblioteki Jetpack mają wbudowaną obsługę funkcji suspend, eliminując potrzebę callback dla standardowych operacji.

kotlin
// Sekwencyjne 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()
            }
        }
    }
}

// Korutyny — rozwiązanie
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: co wybrać?

Callback i Delegate — dwa podejścia do asynchronicznego powiadamiania, a wybór między nimi zależy od wymagań architektonicznych. Callback nadaje się do jednorazowych operacji z jednym wynikiem. Delegate jest przeznaczony do wielu zdarzeń z różnymi sygnaturami metod. Apple zaleca delegate dla złożonych protokołów z wieloma metodami, callback — dla prostych domknięć z jednym wynikiem. W Androidzie callback zastępuje delegate w większości przypadków ze względu na obsługę lambd.

Kiedy wybrać Callback

Callback jest optymalny dla operacji z pojedynczym wynikiem: zapytanie sieciowe, odczyt pliku, animacja z blokiem completion. Zalety: zwięzła składnia, brak osobnego protokołu, bezpośrednie przechwytywanie kontekstu. Wady: złożoność przy wielu wynikach (postęp, pauza, anulowanie), brak możliwości wielokrotnego wysłania (jeśli callback może być wywołany więcej niż raz — użyj publishtera).

Kiedy wybrać Delegate

Delegate nadaje się do protokołów z wieloma obowiązkowymi i opcjonalnymi metodami: UITableViewDelegate, CLLocationManagerDelegate, połączenia Bluetooth. Zalety: czysta typizacja każdej metody, dokumentacja przez protokół, obsługa opcjonalnych metod przez @objc optional. Wady: kod boilerplate, obowiązkowa słaba referencja do delegate (weak var delegate), złożoność przy przechwytywaniu kontekstu.

Często zadawane pytania

Czym różni się callback od funkcji wyższego rzędu?

Callback to szczególny przypadek funkcji wyższego rzędu. Funkcja wyższego rzędu przyjmuje inną funkcję jako argument lub ją zwraca. Callback to funkcja przekazywana specjalnie do asynchronicznego wykonania po zakończeniu operacji. Wszystkie callback są implementowane przez funkcje wyższego rzędu, ale nie każda funkcja wyższego rzędu jest callback.

Czy callback może być wywołany wielokrotnie?

Zgodnie z konwencją callback powinien być wywołany dokładnie raz — albo success, albo failure. Wielokrotne wywołanie tego samego callback jest uważane za błąd projektowy. Dla wielu zdarzeń (postęp, strumień danych) używaj Observable, Publisher lub Flow — obsługują one wielokrotną emisję wartości. Niektóre API naruszają tę zasadę, co prowadzi do trudnych do wykrycia błędów.

Czym jest trailing closure w Swift?

Trailing closure — cukier składniowy Swift, pozwalający przekazać domknięcie po nawiasach okrągłych wywołania funkcji. Jeśli funkcja przyjmuje domknięcie jako ostatni argument, można je umieścić za nawiasami: fetchData { result in ... }. Dla wielu domknięć trailing closure stosuje się tylko do ostatniego, pozostałe są nazywane wewnątrz nawiasów. Poprawia to czytelność API opartych na callback.

Jak uniknąć wycieku pamięci przy callback w Android?

Używaj WeakReference dla długożyjących słuchaczy, anuluj korutyny przez Job.cancel() w onDestroy(), stosuj lifecycleScope do automatycznego anulowania. ViewModel + LiveData/Flow rozwiązują problem na poziomie architektonicznym. Unikaj przekazywania kontekstu Activity do statycznych callback — używaj kontekstu aplikacji. Lambdy Kotlin niejawnie przechwytują this, sprawdzaj przez memory profiler.

Czy async/await całkowicie zastąpi callback?

Async/await zastępuje callback dla sekwencyjnego kodu asynchronicznego, ale nie dla architektury event-driven. Callback pozostaje w systemowych API (View.OnClickListener, delegaty URLSession), wywołaniach zwrotnych z postępem i bibliotekach innych firm. Całkowite zastąpienie jest niemożliwe ze względu na wsteczną kompatybilność. Nowoczesna strategia to używanie async/await z opakowaniami callback (continuation w Swift, suspendCancellableCoroutine w Kotlin).

Podsumowanie

  • Callback — funkcja zwrotna przekazywana jako argument do asynchronicznego wykonania po zakończeniu operacji.
  • Swift implementuje callback przez domknięcia z @escaping, capture list [weak self] i trailing closure syntax.
  • Kotlin używa lambd, funkcji wyższego rzędu i suspend-funkcji korutyn do asynchroniczności.
  • Retain cycle w iOS zapobiega się przez capture list; w Android — przez WeakReference i komponenty Lifecycle-aware.
  • Callback Hell jest rozwiązywany przez async/await w Swift i korutyny z Flow w Kotlin.
  • Delegate jest preferowany nad callback dla protokołów z wieloma metodami; callback — dla jednorazowych operacji.
  • Używaj callback dla prostych operacji asynchronicznych, async/await dla sekwencyjnych łańcuchów, delegate dla wielu zdarzeń.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również