Callback — olyan függvény, amelyet argumentumként adnak át egy másik függvénynek, és az aszinkron művelet befejezése után hajtódik végre. A mobilfejlesztésben a callback a hálózati kérések eredményeinek feldolgozására, adatbázis-műveletekre és animációkra használatos. Az Apple Documentation (2025) szerint a Swiftben a closure-k a callback elsődleges formái, amelyeket URLSession-ben, GCD-ben és Combine-ban használnak. Androidban a callback interfészeken, Kotlin lambda kifejezéseken és ListenableFuture-ön keresztül valósul meg.
Főbb pontok
Callback (visszahívási függvény) — végrehajtható kód, amelyet egy másik függvénynek adnak át, és egy adott művelet befejezése után hívódik meg. A mobilfejlesztésben a callback az aszinkron programozás alapvető mechanizmusa, amely lehetővé teszi a hálózati kérések, időzítők, animációk és be-/kimeneti műveletek befejezésére való reagálást a fő szál blokkolása nélkül. A Swift és a Kotlin beépített szintaktikai konstrukciókat biztosít a callback-ek létrehozásához — closure-ket, illetve lambdákat.
A magasabb rendű függvény egy másik függvényt paraméterként fogad el, és a fő logika végrehajtása után hívja meg. A vezérlés a callback-en keresztül tér vissza a hívóhoz, innen az elnevezés. iOS-ben a callback a UIKit-ben (UIView.animate animációk), Foundation-ben (URLSession.dataTask) és Combine-ban (sink) alkalmazzák. Android-ban a callback a View.OnClickListener-ben, Retrofit Callback-ben és Room DAO-ban használatos. A modern API-k egyre gyakrabban váltják fel a callback-et async/await-re vagy korutinokra, de a callback megértése szükséges az örökölt kód és alacsony szintű API-k kezeléséhez.
A callback lehet szinkron (azonnal meghívódik a függvényen belül) és aszinkron (később hívódik meg egy másik szálból vagy sorból). A szinkron callback-ek rendezésre (komparátorok) és gyűjtemények bejárására használatosak. Aszinkron callback-ek hálózati kérésekre, fájlolvasásra és érzékelőkkel való munkára alkalmazzák. A különbség kritikus a szálkezelés megértéséhez: a szinkron callback ugyanabban a szálban fut, az aszinkron — a diszpécser által meghatározott szálban (DispatchQueue iOS-ben, Dispatchers Kotlin-ban).
A callback mechanizmus mindkét platformon ugyanazon az elven alapul: a függvény első osztályú objektumként kerül átadásra, és a végrehajtás pillanatáig tárolódik. A megvalósítások azonban eltérnek a különböző nyelvi paradigmák miatt. iOS-ben a callback egy closure, amely változókat fog el a környező kontextusból. Android-ban a callback leggyakrabban névtelen osztályokon vagy Kotlin lambda kifejezéseken keresztül valósul meg, amelyek FunctionalInterface-vé fordulnak.
Amikor egy aszinkron függvény meghívódik, a closure a heap-en tárolódik az elfogott változókkal együtt. Amikor a művelet befejeződik, a GCD vagy OperationQueue rendszer elhelyezi a callback-et a megfelelő sorban (main queue vagy background queue). A végrehajtás után a callback törlődik a memóriából, ha nincsenek erős referenciák. Capture list ([weak self]) megakadályozza az objektum megtartását a felszabadítás után. Capture list nélkül retain cycle alakul ki, ahol az objektum és a callback kölcsönösen hivatkoznak egymásra.
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()
}
// Használat [weak self]-fel
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-ban a callback interfészen vagy lambdán keresztül adódik át. Amikor egy aszinkron művelet végrehajtódik ExecutorService vagy korutin által, a callback a memóriában tárolódik a háttérmunka befejezéséig. Kotlin lambdák névtelen osztályokká fordulnak, amelyek elfogják a külső változókat. A gyenge referenciák hiánya a JVM-ben kézi kezelést igényel: a callback nullázása onDestroy()-ban vagy a korutinok megszakítása Job.cancel()-lel. A ViewModel és LiveData ezt a problémát architektúrális komponens szinten oldja meg.
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) }
}
}
}
}
// Használat lambdával
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
A callback szintaxisát a függvények első osztályú objektumként való kezelésének nyelvi képességei határozzák meg. Swift-ben a closure-k rövid szintaxissal rendelkeznek automatikus argumentumnevekkel ($0, $1). Kotlin-ban a lambdák szintén támogatják az it-t egyetlen argumentum esetén. A különbségek a változóelfogás kezelésében (capture list Swift-ben vs változtatható referenciák Kotlin-ban) és a típusosságban (Result<Success, Failure> vs Result<T>) nyilvánulnak meg.
A Swift closure egy önálló kódblokk, amely átadható és használható egy másik függvényben. A closure-k lehetnek globális (nevesített), beágyazott és expression-level. @escaping jelöli azt a closure-t, amely a függvényből való visszatérés után hajtódik végre — ez kötelező követelmény az aszinkron callback-eknél. @escaping nélkül a closure csak a függvény törzsén belül hajtható végre. A trailing closure szintaxis lehetővé teszi a closure átadását a kerek zárójelek után: 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)
}
}
A Kotlin támogatja a magasabb rendű függvényeket, amelyek más függvényeket fogadnak el paraméterként. Callback Kotlin-ban (T) -> Unit típusú paraméteren vagy (T) -> R típuson keresztül adódik át a visszatérési értékhez. A Kotlin korutinok suspend függvényei helyettesítik a callback-et szekvenciális kóddal, de a callback megmarad a Java-kompatibilis API-kban és az Android SDK-ban (View.setOnClickListener, TextWatcher). A Kotlin lambdák automatikusan elfogják a val változókat, a var változók változtatható burkolókat igényelnek.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Példa lambdával
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — olyan helyzet, amikor két objektum erős referenciákat tart fenn egymásra, megakadályozva a memória felszabadítását a garbage collector által. Swift-ben retain cycle akkor keletkezik, amikor a viewController elfog egy closure-t, és a closure elfogja a self-et. Kotlin/Java-ban szivárgás akkor történik, amikor az Activity egy belső osztályt vagy lambdát ad át egy hosszú háttérműveletnek. A WWDC Session 10216 (2024) szerint a closure-k helytelen kezelése a harmadik leggyakoribb memóriaszivárgási ok iOS alkalmazásokban.
A Swift Automatic Reference Counting (ARC) rendszert használ, amely felszabadítja az objektumot, amikor a referenciaszámláló nullázódik. Capture list [weak self] vagy [unowned self] a closure-ben megakadályozza a retain cycle-t. A weak self opcionális referenciát hoz létre, amely nil lesz az objektum felszabadításakor. Az unowned self feltételezi, hogy az objektum tovább él, mint a closure — ennek megsértése crash-t okoz. A weak self használata biztonságos alapértelmezett opcióként ajánlott.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Javítva: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
Android-ban callback-szivárgás akkor következik be, amikor az Activity vagy Fragment egy listener-t ad át egy singleton komponensnek (pl. EventBus vagy Service). WeakReference lehetővé teszi a garbage collector számára az Activity felszabadítását, még akkor is, ha gyenge referencia mutat rá. Az Lifecycle-aware komponensek (LiveData, Flow) automatikusan megoldják a problémát. A Kotlin lambdák, amelyek Activity kontextust fognak el, szintén okozhatnak szivárgást: a lambda implicit módon tárol egy referenciát a this-re.
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()
}
}
}
// Használat Fragment-ben
manager.addListener { result ->
// WeakReference nem tartja meg a Fragment-et
updateUI(result)
}
Callback Hell (más néven a Végzet Piramisa) — olyan helyzet, amikor számos beágyazott callback mélyen beágyazott kódszerkezetet hoz létre, amely nehezen olvasható és hibakereshető. Minden következő lépés az előző befejezésének megvárását igényli, ami 5-10 szintű beágyazottsághoz vezet. Ez a probléma jellemző a szekvenciális aszinkron műveletekre: adatok betöltése → feldolgozás → mentés adatbázisba → UI frissítése.
A Swift 5.5 bevezette az aszinkron függvényeket (async/await), amelyek lehetővé teszik az aszinkron kód szekvenciális írását. AsyncSequence és AsyncStream helyettesítik a callback-alapú iterációkat. A Combine keretrendszer flatMap, merge, combineLatest operátorokat biztosít aszinkron folyamok kompozíciójához beágyazás nélkül. A callback azonban továbbra is szükséges az Objective-C API-kkal és async támogatás nélküli külső könyvtárakkal való munkához.
// Beágyazott callback-ek — 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 — megoldás
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)
}
A Kotlin korutinok helyettesítik a callback-et suspend függvényekkel szekvenciális végrehajtással. Flow hideg folyamokat biztosít map, flatMapConcat, combine operátorokkal. A CoroutineScope lehetővé teszi az összes elindított korutin megszakítását a komponens megsemmisülésekor. A Room, Retrofit és más Jetpack könyvtárak beépített támogatással rendelkeznek a suspend függvényekhez, kiküszöbölve a callback szükségességét a szabványos műveletekhez.
// Szekvenciális callback-ek — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Korutinok — megoldás
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 és Delegate — két megközelítés az aszinkron értesítéshez, és a köztük való választás az architekturális követelményektől függ. A callback egyszeri műveletekhez alkalmas egyetlen eredménnyel. A delegate több eseményhez készült eltérő metódusaláírásokkal. Az Apple a delegate-t ajánlja összetett, több metódust tartalmazó protokollokhoz, a callback-et — egyszerű, egyetlen eredményt adó closure-khez. Android-ban a callback a legtöbb esetben helyettesíti a delegate-et a lambda-támogatás miatt.
A callback optimális egyetlen eredménnyel járó műveletekhez: hálózati kérés, fájlolvasás, animáció completion blokkal. Előnyök: kompakt szintaxis, nincs külön protokoll, közvetlen kontextuselfogás. Hátrányok: komplexitás több eredmény esetén (haladás, szünet, megszakítás), többszöri küldés lehetetlensége (ha a callback többször is meghívható — használjon publishtert).
A delegate több kötelező és opcionális metódust tartalmazó protokollokhoz alkalmas: UITableViewDelegate, CLLocationManagerDelegate, Bluetooth kapcsolatok. Előnyök: minden metódus világos típusossága, dokumentáció protokollon keresztül, opcionális metódusok támogatása @objc optional segítségével. Hátrányok: boilerplate kód, kötelező gyenge referencia a delegate-hez (weak var delegate), komplexitás a kontextuselfogásban.
Gyakran Ismételt Kérdések
Callback a magasabb rendű függvény egy speciális esete. A magasabb rendű függvény egy másik függvényt fogad el argumentumként vagy ad vissza. A callback egy olyan függvény, amelyet kifejezetten aszinkron végrehajtásra adnak át a művelet befejezése után. Minden callback magasabb rendű függvényeken keresztül valósul meg, de nem minden magasabb rendű függvény callback.
Megállapodás szerint a callback-nek pontosan egyszer kell meghívódnia — vagy success, vagy failure. Többszöri meghívás ugyanazon callback esetében tervezési hibának számít. Több eseményhez (haladás, adatfolyam) használjon Observable-t, Publisher-t vagy Flow-t — ezek támogatják a többszörös értékkibocsátást. Egyes API-k megsértik ezt a szabályt, ami nehezen felderíthető hibákhoz vezet.
Trailing closure — szintaktikai cukor Swift-ben, amely lehetővé teszi a closure átadását a függvényhívás kerek zárójelei után. Ha a függvény a closure-t utolsó argumentumként fogadja el, az a zárójeleken kívül helyezhető: fetchData { result in ... }. Több closure esetén a trailing closure csak az utolsóra alkalmazandó, a többit a zárójeleken belül nevezzük meg. Ez javítja a callback-alapú API-k olvashatóságát.
Használjon WeakReference-t hosszú életű listener-ekhez, szakítsa meg a korutinokat Job.cancel()-lel onDestroy()-ban, alkalmazzon lifecycleScope-ot automatikus megszakításhoz. A ViewModel + LiveData/Flow architektúrális szinten oldja meg a problémát. Kerülje az Activity kontextus átadását statikus callback-ekbe — használjon Application kontextust. A Kotlin lambdák implicit módon elfogják a this-t, ellenőrizze memory profiler-rel.
Async/await leváltja a callback-et a szekvenciális aszinkron kódhoz, de nem az event-driven architektúrához. A callback megmarad a rendszer API-kban (View.OnClickListener, URLSession delegate-ek), a haladással rendelkező visszahívásokban és külső könyvtárakban. A teljes leváltás lehetetlen a visszamenőleges kompatibilitás miatt. A modern stratégia az async/await használata callback burkolókkal (continuation Swift-ben, suspendCancellableCoroutine Kotlin-ban).
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is