Callback — is een functie die als argument aan een andere functie wordt doorgegeven en wordt uitgevoerd na voltooiing van een asynchrone bewerking. In mobiele ontwikkeling wordt callback gebruikt voor het verwerken van netwerkverzoekresultaten, databasewerk en animaties. Volgens Apple Documentation (2025) zijn closures in Swift de primaire vorm van callback en worden ze gebruikt in URLSession, GCD en Combine. In Android wordt callback geïmplementeerd via interfaces, Kotlin lambdas en ListenableFuture.
Belangrijkste punten
Callback (callback-functie) — is uitvoerbare code die aan een andere functie wordt doorgegeven en wordt aangeroepen na voltooiing van een specifieke actie. In mobiele ontwikkeling is callback het fundamentele mechanisme van asynchroon programmeren, waarmee kan worden gereageerd op voltooiing van netwerkverzoeken, timers, animaties en I/O-bewerkingen zonder de hoofdthread te blokkeren. Swift en Kotlin bieden ingebouwde syntactische constructies voor het maken van callbacks — respectievelijk closures en lambdas.
Een hogere-orde functie accepteert een andere functie als parameter en roept deze aan na het uitvoeren van de hoofdlogica. De controlestroom keert terug naar de aanroeper via de callback, vandaar de naam. In iOS wordt callback toegepast in UIKit (UIView.animate-animaties), Foundation (URLSession.dataTask) en Combine (sink). In Android wordt callback gebruikt in View.OnClickListener, Retrofit Callback en Room DAO. Moderne API's vervangen steeds vaker callback door async/await of coroutines, maar begrip van callback is noodzakelijk voor het werken met legacy-code en low-level API's.
Callback kan synchroon (onmiddellijk aangeroepen binnen de functie) en asynchroon (later aangeroepen vanuit een andere thread of wachtrij) zijn. Synchrone callbacks worden gebruikt voor sorteren (comparators) en het doorlopen van collecties. Asynchrone callbacks worden toegepast voor netwerkverzoeken, bestandslezen en werken met sensoren. Het verschil is cruciaal voor het begrijpen van threading: een synchrone callback wordt uitgevoerd in dezelfde thread, een asynchrone — in de thread bepaald door de dispatcher (DispatchQueue in iOS, Dispatchers in Kotlin).
Het callback-mechanisme op beide platforms is gebaseerd op hetzelfde principe: de functie wordt doorgegeven als een first-class object en opgeslagen tot het moment van uitvoering. De implementaties verschillen echter vanwege verschillende taalparadigma's. In iOS is een callback een closure die variabelen uit de omringende context vastlegt. In Android wordt callback meestal geïmplementeerd via anonieme klassen of Kotlin lambda-expressies, gecompileerd naar FunctionalInterface.
Bij het aanroepen van een asynchrone functie wordt de closure op de heap opgeslagen samen met de vastgelegde variabelen. Wanneer de bewerking is voltooid, plaatst het GCD- of OperationQueue-systeem de callback in de juiste wachtrij (main queue of background queue). Na uitvoering wordt de callback uit het geheugen verwijderd bij afwezigheid van sterke referenties. Capture list ([weak self]) voorkomt dat het object na deallocatie wordt vastgehouden. Zonder capture list ontstaat een retain cycle, waarbij het object en de callback naar elkaar verwijzen.
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()
}
// Gebruik met [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)
}
}
In Android wordt de callback doorgegeven via een interface of lambda. Bij het uitvoeren van een asynchrone bewerking via ExecutorService of een coroutine, wordt de callback in het geheugen opgeslagen tot de achtergrondtaak is voltooid. Kotlin-lambdas worden gecompileerd naar anonieme klassen die externe variabelen vastleggen. De afwezigheid van zwakke referenties in de JVM vereist handmatig beheer: de callback resetten in onDestroy() of coroutines annuleren via Job.cancel(). ViewModel en LiveData lossen dit probleem op architectuurniveau op.
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) }
}
}
}
}
// Gebruik met lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
De callback-syntaxis wordt bepaald door de taalcapaciteiten om met functies als first-class objecten te werken. In Swift hebben closures een beknopte syntaxis met automatische argumentnamen ($0, $1). In Kotlin ondersteunen lambdas ook it voor een enkel argument. Verschillen manifesteren zich in de afhandeling van variabele vastlegging (capture list in Swift versus veranderlijke referenties in Kotlin) en typering (Result<Success, Failure> versus Result<T>).
Een Swift-closure is een zelfstandig codeblok dat kan worden doorgegeven en gebruikt in een andere functie. Closures kunnen globaal (benoemd), genest en expression-level zijn. @escaping markeert een closure die wordt uitgevoerd na terugkeer uit de functie — dit is een verplichte vereiste voor asynchrone callbacks. Zonder @escaping kan de closure alleen binnen de functiebody worden uitgevoerd. Trailing closure-syntaxis maakt het mogelijk een closure na de ronde haakjes door te geven: 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 ondersteunt hogere-orde functies die andere functies als parameters accepteren. Callback wordt in Kotlin doorgegeven via een parameter van het type (T) -> Unit of (T) -> R voor een retourwaarde. Suspend-functies van Kotlin-coroutines vervangen callback door sequentiële code, maar callback blijft bestaan in Java-compatibele API's en de Android SDK (View.setOnClickListener, TextWatcher). Kotlin-lambdas leggen automatisch val-variabelen vast, var-variabelen vereisen veranderlijke wrappers.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Voorbeeld met lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — een situatie waarin twee objecten sterke referenties naar elkaar behouden, waardoor ze niet door de garbage collector kunnen worden vrijgegeven. In Swift ontstaat een retain cycle wanneer een viewController een closure vastlegt en de closure self vastlegt. In Kotlin/Java treedt een lek op wanneer Activity een inner class of lambda doorgeeft aan een langlopende achtergrondbewerking. Volgens WWDC Session 10216 (2024) is onjuist beheer van closures de derde meest voorkomende oorzaak van geheugenlekken in iOS-applicaties.
Swift gebruikt Automatic Reference Counting (ARC), die het object vrijgeeft bij het nulstellen van de referentieteller. Capture list [weak self] of [unowned self] in de closure voorkomt retain cycle. weak self creëert een optionele referentie die nil wordt bij deallocatie van het object. unowned self veronderstelt dat het object langer leeft dan de closure — schending van deze veronderstelling veroorzaakt een crash. Het gebruik van weak self als veilige standaardoptie wordt aanbevolen.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Gecorrigeerd: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
In Android treedt een callback-lek op wanneer Activity of Fragment een listener doorgeeft aan een singleton-component (bijv. EventBus of Service). WeakReference stelt de garbage collector in staat Activity vrij te geven, zelfs als er een zwakke referentie naar bestaat. Lifecycle-aware componenten (LiveData, Flow) lossen het probleem automatisch op. Kotlin-lambdas die Activity-context vastleggen, kunnen ook lekkage veroorzaken: de lambda houdt impliciet een referentie naar this vast.
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()
}
}
}
// Gebruik in Fragment
manager.addListener { result ->
// WeakReference behoudt Fragment niet
updateUI(result)
}
Callback Hell (ook bekend als de Pyramid of Doom) — is een situatie waarin talrijke geneste callbacks een diep geneste codestructuur creëren die moeilijk te lezen en debuggen is. Elke volgende stap vereist wachten op voltooiing van de vorige, wat leidt tot 5-10 niveaus van nesting. Dit probleem is kenmerkend voor sequentiële asynchrone bewerkingen: gegevens laden → parseren → opslaan in database → UI bijwerken.
Swift 5.5 introduceerde asynchrone functies (async/await) die het mogelijk maken asynchrone code sequentieel te schrijven. AsyncSequence en AsyncStream vervangen op callback gebaseerde iteraties. Het Combine-framework biedt operators flatMap, merge, combineLatest voor compositie van asynchrone stromen zonder nesting. Callback blijft echter noodzakelijk voor het werken met Objective-C API's en bibliotheken van derden zonder async-ondersteuning.
// Geneste callbacks — 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 — oplossing
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-coroutines vervangen callback door suspend-functies met sequentiële uitvoering. Flow biedt cold-streams met operators map, flatMapConcat, combine. CoroutineScope maakt het mogelijk alle gestarte coroutines te annuleren bij vernietiging van de component. Room, Retrofit en andere Jetpack-bibliotheken hebben ingebouwde ondersteuning voor suspend-functies, waardoor de noodzaak voor callbacks voor standaardbewerkingen wordt geëlimineerd.
// Sequentiële callbacks — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Coroutines — oplossing
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 en Delegate — twee benaderingen voor asynchrone melding, en de keuze ertussen hangt af van architectuurvereisten. Callback is geschikt voor eenmalige bewerkingen met één resultaat. Delegate is bedoeld voor meerdere gebeurtenissen met verschillende methodehandtekeningen. Apple beveelt delegate aan voor complexe protocollen met meerdere methoden, callback — voor eenvoudige closures met één resultaat. In Android vervangt callback in de meeste gevallen delegate vanwege de ondersteuning voor lambdas.
Callback is optimaal voor bewerkingen met één resultaat: netwerkverzoek, bestandslezen, animatie met een completion-blok. Voordelen: compacte syntaxis, geen apart protocol, directe contextvastlegging. Nadelen: complexiteit bij meerdere resultaten (voortgang, pauze, annulering), onmogelijkheid van meerdere verzendingen (als de callback meer dan één keer kan worden aangeroepen — gebruik een publisher).
Delegate is geschikt voor protocollen met meerdere verplichte en optionele methoden: UITableViewDelegate, CLLocationManagerDelegate, Bluetooth-verbindingen. Voordelen: duidelijke typering van elke methode, documentatie via protocol, ondersteuning voor optionele methoden via @objc optional. Nadelen: boilerplate-code, verplichte zwakke referentie naar delegate (weak var delegate), complexiteit bij contextvastlegging.
Veelgestelde vragen
Callback is een speciaal geval van een hogere-orde functie. Een hogere-orde functie accepteert een andere functie als argument of retourneert deze. Callback is een functie die specifiek wordt doorgegeven voor asynchrone uitvoering na voltooiing van de bewerking. Alle callbacks worden geïmplementeerd via hogere-orde functies, maar niet elke hogere-orde functie is een callback.
Volgens afspraak moet een callback precies één keer worden aangeroepen — ofwel success ofwel failure. Meerdere aanroepen van dezelfde callback wordt beschouwd als een ontwerpfout. Voor meerdere gebeurtenissen (voortgang, gegevensstroom) gebruik Observable, Publisher of Flow — deze ondersteunen meerdere waarde-emissies. Sommige API's schenden deze regel, wat leidt tot moeilijk te traceren bugs.
Trailing closure — syntaxsuiker in Swift die het mogelijk maakt een closure na de ronde haakjes van de functieaanroep door te geven. Als een functie een closure als laatste argument accepteert, kan deze buiten de haakjes worden geplaatst: fetchData { result in ... }. Voor meerdere closures wordt trailing closure alleen toegepast op de laatste, de overige worden binnen de haakjes benoemd. Dit verbetert de leesbaarheid van op callback gebaseerde API's.
Gebruik WeakReference voor langlevende listeners, annuleer coroutines via Job.cancel() in onDestroy(), pas lifecycleScope toe voor automatische annulering. ViewModel + LiveData/Flow lossen het probleem op architectuurniveau op. Vermijd het doorgeven van Activity-context in statische callbacks — gebruik Application context. Kotlin-lambdas leggen impliciet this vast, controleer met memory profiler.
Async/await vervangt callback voor sequentiële asynchrone code, maar niet voor event-driven architectuur. Callback blijft bestaan in systeem-API's (View.OnClickListener, URLSession-delegaten), callbacks met voortgang en bibliotheken van derden. Volledige vervanging is onmogelijk vanwege achterwaartse compatibiliteit. De moderne strategie is het gebruik van async/await met callback-wrappers (continuation in Swift, suspendCancellableCoroutine in Kotlin).
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook