Callback ist eine Funktion, die als Argument an eine andere Funktion übergeben und nach Abschluss einer asynchronen Operation ausgeführt wird. In der mobilen Entwicklung wird Callback zur Verarbeitung von Netzwerkanfrageergebnissen, Datenbankoperationen und Animationen verwendet. Laut Apple Documentation (2025) sind Closures in Swift die primäre Form von Callbacks und werden in URLSession, GCD und Combine verwendet. In Android wird Callback über Interfaces, Kotlin-Lambdas und ListenableFuture implementiert.
Wichtigste Punkte
Callback (Rückruffunktion) ist ausführbarer Code, der an eine andere Funktion übergeben und nach Abschluss einer bestimmten Aktion aufgerufen wird. In der mobilen Entwicklung ist Callback ein grundlegender Mechanismus der asynchronen Programmierung, der es ermöglicht, auf den Abschluss von Netzwerkanfragen, Timern, Animationen und E/A-Operationen zu reagieren, ohne den Hauptthread zu blockieren. Swift und Kotlin bieten integrierte syntaktische Konstrukte zum Erstellen von Callbacks — Closures bzw. Lambdas.
Eine Funktion höherer Ordnung akzeptiert eine andere Funktion als Parameter und ruft sie nach Ausführung ihrer Hauptlogik auf. Der Kontrollfluss wird durch den Callback an den Aufrufer zurückgegeben, daher der Name. In iOS wird Callback in UIKit (UIView.animate-Animationen), Foundation (URLSession.dataTask) und Combine (sink) verwendet. In Android wird Callback in View.OnClickListener, Retrofit Callback und Room DAO verwendet. Moderne APIs ersetzen zunehmend Callbacks durch async/await oder Koroutinen, aber das Verständnis von Callbacks ist für die Arbeit mit Legacy-Code und Low-Level-APIs erforderlich.
Callbacks können synchron (sofort innerhalb der Funktion aufgerufen) und asynchron (später von einem anderen Thread oder einer anderen Warteschlange aufgerufen) sein. Synchrone Callbacks werden zum Sortieren (Comparatoren) und zum Durchlaufen von Sammlungen verwendet. Asynchrone Callbacks werden für Netzwerkanfragen, Dateilesen und die Arbeit mit Sensoren verwendet. Der Unterschied ist für das Verständnis von Threading entscheidend: Der synchrone Callback wird im selben Thread ausgeführt, der asynchrone in einem vom Dispatcher bestimmten Thread (DispatchQueue in iOS, Dispatchers in Kotlin).
Der Callback-Mechanismus auf beiden Plattformen basiert auf dem gleichen Prinzip: Eine Funktion wird als First-Class-Objekt übergeben und bis zum Ausführungszeitpunkt gespeichert. Die Implementierungen unterscheiden sich jedoch aufgrund unterschiedlicher Sprachparadigmen. In iOS ist ein Callback ein Closure, das Variablen aus dem umgebenden Kontext erfasst. In Android wird ein Callback meist über anonyme Klassen oder Kotlin-Lambda-Ausdrücke implementiert, die in FunctionalInterface kompiliert werden.
Beim Aufruf einer asynchronen Funktion wird das Closure zusammen mit den erfassten Variablen auf dem Heap gespeichert. Wenn die Operation abgeschlossen ist, platziert das GCD- oder OperationQueue-System den Callback in der entsprechenden Warteschlange (Hauptwarteschlange oder Hintergrundwarteschlange). Nach der Ausführung wird der Callback aus dem Speicher entfernt, wenn keine starken Referenzen mehr vorhanden sind. Die Capture-List ([weak self]) verhindert das Halten des Objekts nach seiner Freigabe. Ohne Capture-List entsteht ein Retain Cycle, bei dem Objekt und Callback aufeinander verweisen.
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()
}
// Verwendung mit [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 wird der Callback über ein Interface oder Lambda übergeben. Bei der Ausführung einer asynchronen Operation über ExecutorService oder eine Koroutine wird der Callback im Speicher gehalten, bis die Hintergrundarbeit abgeschlossen ist. Kotlin-Lambdas werden in anonyme Klassen kompiliert, die externe Variablen erfassen. Das Fehlen schwacher Referenzen in der JVM erfordert manuelle Verwaltung: Nullsetzen des Callbacks in onDestroy() oder Abbrechen von Koroutinen über Job.cancel(). ViewModel und LiveData lösen dieses Problem auf architektonischer Komponentenebene.
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) }
}
}
}
}
// Verwendung mit Lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
Die Callback-Syntax wird durch die Fähigkeit der Sprache bestimmt, Funktionen als First-Class-Objekte zu behandeln. In Swift haben Closures eine prägnante Syntax mit automatischen Argumentnamen ($0, $1). In Kotlin unterstützen Lambdas it für ein einzelnes Argument. Unterschiede zeigen sich bei der Behandlung der Variablenerfassung (Capture-List in Swift vs. veränderbare Referenzen in Kotlin) und der Typisierung (Result
Ein Swift-Closure ist ein eigenständiger Codeblock, der an eine andere Funktion übergeben und dort verwendet werden kann. Closures können global (benannt), verschachtelt und auf Ausdrucksebene sein. @escaping markiert ein Closure, das nach der Rückkehr der Funktion ausgeführt wird — dies ist eine zwingende Anforderung für asynchrone Callbacks. Ohne @escaping kann das Closure nur innerhalb des Funktionskörpers ausgeführt werden. Die Trailing-Closure-Syntax ermöglicht die Übergabe des Closures nach den Klammern: 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 unterstützt Funktionen höherer Ordnung, die andere Funktionen als Parameter akzeptieren. Callback wird in Kotlin über einen Parameter vom Typ (T) -> Unit oder (T) -> R für Rückgabewerte übergeben. suspend-Funktionen von Kotlin-Koroutinen ersetzen Callbacks durch sequenziellen Code, aber Callbacks bleiben in Java-kompatiblen APIs und im Android SDK (View.setOnClickListener, TextWatcher) erhalten. Kotlin-Lambdas erfassen automatisch val-Variablen, var-Variablen erfordern Mutability-Wrapper.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Beispiel mit Lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle ist eine Situation, in der zwei Objekte starke Referenzen aufeinander halten und der Speicherverwaltung ihre Freigabe verhindern. In Swift entsteht ein Retain Cycle, wenn ein viewController ein Closure erfasst und das Closure self erfasst. In Kotlin/Java tritt ein Leck auf, wenn eine Activity eine innere Klasse oder ein Lambda an eine langlaufende Hintergrundoperation übergibt. Laut WWDC Session 10216 (2024) ist unsachgemäßes Closure-Management die dritthäufigste Ursache für Speicherlecks in iOS-Anwendungen.
Swift verwendet Automatic Reference Counting (ARC), das ein Objekt freigibt, wenn der Referenzzähler null erreicht. Die Capture-List [weak self] oder [unowned self] in einem Closure verhindert Retain Cycles. weak self erzeugt eine optionale Referenz, die bei der Freigabe des Objekts nil wird. unowned self geht davon aus, dass das Objekt länger lebt als das Closure — bei Verletzung dieser Annahme kommt es zu einem Crash. Die Verwendung von weak self als sichere Standardeinstellung wird empfohlen.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Behoben: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
In Android tritt ein Callback-Speicherleck auf, wenn eine Activity oder Fragment einen Listener an eine Singleton-Komponente (z. B. EventBus oder Service) übergibt. WeakReference ermöglicht es dem Garbage Collector, die Activity freizugeben, selbst wenn eine schwache Referenz darauf existiert. Lifecycle-aware-Komponenten (LiveData, Flow) lösen das Problem automatisch. Kotlin-Lambdas, die den Activity-Kontext erfassen, können ebenfalls Lecks verursachen: Das Lambda speichert implizit eine Referenz auf 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()
}
}
}
// Verwendung in Fragment
manager.addListener { result ->
// WeakReference hält Fragment nicht
updateUI(result)
}
Callback Hell (auch bekannt als Pyramid of Doom) ist eine Situation, in der viele verschachtelte Callbacks eine tief verschachtelte Codestruktur erzeugen, die schwer zu lesen und zu debuggen ist. Jeder folgende Schritt erfordert das Warten auf den Abschluss des vorherigen, was zu 5-10 Verschachtelungsebenen führt. Dieses Problem ist typisch für sequenzielle asynchrone Operationen: Daten laden → Parsen → in DB speichern → UI aktualisieren.
Swift 5.5 führte asynchrone Funktionen (async/await) ein, die das sequenzielle Schreiben von asynchronem Code ermöglichen. AsyncSequence und AsyncStream ersetzen callback-basierte Iterationen. Das Combine-Framework bietet Operatoren wie flatMap, merge, combineLatest zur Komposition asynchroner Streams ohne Verschachtelung. Callbacks bleiben jedoch für die Arbeit mit Objective-C-APIs und Drittanbieterbibliotheken ohne Async-Unterstützung erforderlich.
// Verschachtelte 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 — Lösung
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-Koroutinen ersetzen Callbacks durch suspend-Funktionen für sequenzielle Ausführung. Flow bietet kalte Streams mit Operatoren wie map, flatMapConcat, combine. CoroutineScope ermöglicht das Abbrechen aller laufenden Koroutinen beim Zerstören einer Komponente. Room, Retrofit und andere Jetpack-Bibliotheken haben integrierte Unterstützung für suspend-Funktionen, wodurch Callbacks für Standardoperationen überflüssig werden.
// Sequenzielle 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()
}
}
}
}
// Koroutinen — Lösung
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 und Delegate sind zwei Ansätze für asynchrone Benachrichtigungen, und die Wahl hängt von den architektonischen Anforderungen ab. Callback eignet sich für einmalige Operationen mit einem einzigen Ergebnis. Delegate ist für mehrere Ereignisse mit unterschiedlichen Methodensignaturen konzipiert. Apple empfiehlt Delegate für komplexe Protokolle mit mehreren Methoden und Callback für einfache Closures mit einem einzigen Ergebnis. In Android ersetzt Callback aufgrund der Lambda-Unterstützung in den meisten Fällen Delegate.
Callback ist optimal für Operationen mit einem einzigen Ergebnis: Netzwerkanfrage, Dateilesen, Animation mit Completion-Block. Vorteile: kompakte Syntax, kein separates Protokoll, direkte Kontexterfassung. Nachteile: Komplexität bei mehreren Ergebnissen (Fortschritt, Pause, Abbruch), Unmöglichkeit des Mehrfachversands (wenn ein Callback mehrfach aufgerufen werden kann — verwenden Sie einen Publisher).
Delegate eignet sich für Protokolle mit mehreren erforderlichen und optionalen Methoden: UITableViewDelegate, CLLocationManagerDelegate, Bluetooth-Verbindungen. Vorteile: klare Typisierung jeder Methode, Dokumentation durch das Protokoll, Unterstützung optionaler Methoden über @objc optional. Nachteile: Boilerplate-Code, schwache Referenz auf Delegate zwingend erforderlich (weak var delegate), Komplexität bei der Kontexterfassung.
Häufig gestellte Fragen
Callback ist ein Sonderfall einer Funktion höherer Ordnung. Eine Funktion höherer Ordnung akzeptiert eine andere Funktion als Argument oder gibt sie zurück. Ein Callback ist eine Funktion, die speziell für die asynchrone Ausführung nach Abschluss einer Operation übergeben wird. Alle Callbacks werden durch Funktionen höherer Ordnung implementiert, aber nicht jede Funktion höherer Ordnung ist ein Callback.
Gemäß Konvention sollte ein Callback genau einmal aufgerufen werden — entweder success oder failure. Der mehrfache Aufruf desselben Callbacks gilt als Designfehler. Für mehrere Ereignisse (Fortschritt, Datenstrom) verwenden Sie Observable, Publisher oder Flow — sie unterstützen mehrere Wertemissionen. Einige APIs verstoßen gegen diese Regel, was zu schwer auffindbaren Fehlern führt.
Trailing Closure ist syntaktischer Zucker in Swift, der es ermöglicht, ein Closure nach den runden Klammern des Funktionsaufrufs zu übergeben. Wenn eine Funktion ein Closure als letztes Argument akzeptiert, kann es außerhalb der Klammern platziert werden: fetchData { result in ... }. Bei mehreren Closures wird Trailing Closure nur auf das letzte angewendet; die übrigen werden innerhalb der Klammern benannt. Dies verbessert die Lesbarkeit callback-basierter APIs.
Verwenden Sie WeakReference für langlebige Listener, brechen Sie Koroutinen über Job.cancel() in onDestroy() ab, verwenden Sie lifecycleScope für automatischen Abbruch. ViewModel + LiveData/Flow löst das Problem auf architektonischer Ebene. Vermeiden Sie die Übergabe von Activity-Kontext an statische Callbacks — verwenden Sie Application Context. Kotlin-Lambdas erfassen this implizit, überprüfen Sie mit einem Memory Profiler.
Async/await ersetzt Callbacks für sequenziellen asynchronen Code, aber nicht für ereignisgesteuerte Architekturen. Callbacks bleiben in System-APIs (View.OnClickListener, URLSession-Delegaten), Fortschritts-Callbacks und Drittanbieterbibliotheken erhalten. Eine vollständige Ersetzung ist aufgrund der Abwärtskompatibilität unmöglich. Die moderne Strategie ist die Verwendung von async/await mit Callback-Wrappern (Continuation in Swift, suspendCancellableCoroutine in Kotlin).
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch