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) — 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.
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.
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).
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.
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ą.
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)
}
}
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.
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 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>).
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 ... }.
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 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.
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 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.
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.
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) { }
}
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.
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 (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.
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.
// 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)
}
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.
// 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 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.
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).
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
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.
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.
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.
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.
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
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.
Przeczytaj również