Callback est une fonction qui est passée à une autre fonction en tant qu'argument et exécutée après la fin d'une opération asynchrone. Dans le développement mobile, le callback est utilisé pour traiter les résultats de requêtes réseau, les opérations de base de données et les animations. Selon la Documentation Apple (2025), les closures en Swift sont la forme principale de callback et sont utilisées dans URLSession, GCD et Combine. Dans Android, le callback est implémenté via des interfaces, des lambdas Kotlin et ListenableFuture.
Points Clés
Callback (fonction de rappel) est un code exécutable qui est passé à une autre fonction et appelé après l'achèvement d'une action spécifique. Dans le développement mobile, le callback est un mécanisme fondamental de la programmation asynchrone, permettant de réagir à l'achèvement de requêtes réseau, de temporisateurs, d'animations et d'opérations d'E/S sans bloquer le thread principal. Swift et Kotlin fournissent des constructions syntaxiques intégrées pour créer des callbacks — des closures et des lambdas respectivement.
Une fonction d'ordre supérieur accepte une autre fonction comme paramètre et l'appelle après avoir exécuté sa logique principale. Le flux de contrôle est renvoyé à l'appelant via le callback, d'où le nom. Dans iOS, le callback est utilisé dans UIKit (animations UIView.animate), Foundation (URLSession.dataTask) et Combine (sink). Dans Android, le callback est utilisé dans View.OnClickListener, Retrofit Callback et Room DAO. Les API modernes remplacent de plus en plus les callbacks par async/await ou des coroutines, mais comprendre les callbacks est nécessaire pour travailler avec du code legacy et des API de bas niveau.
Le callback peut être synchrone (appelé immédiatement dans la fonction) et asynchrone (appelé plus tard depuis un autre thread ou une autre file d'attente). Les callbacks synchrones sont utilisés pour le tri (comparateurs) et le parcours de collections. Les callbacks asynchrones sont utilisés pour les requêtes réseau, la lecture de fichiers et le travail avec les capteurs. La différence est cruciale pour comprendre le threading : le callback synchrone s'exécute dans le même thread, le callback asynchrone dans un thread déterminé par le dispatcher (DispatchQueue dans iOS, Dispatchers dans Kotlin).
Le mécanisme de callback sur les deux plateformes est basé sur le même principe : une fonction est passée comme objet de première classe et stockée jusqu'au moment de l'exécution. Cependant, les implémentations diffèrent en raison de paradigmes linguistiques différents. Dans iOS, un callback est une closure qui capture des variables du contexte environnant. Dans Android, un callback est le plus souvent implémenté via des classes anonymes ou des expressions lambda Kotlin, compilées en FunctionalInterface.
Lors de l'appel d'une fonction asynchrone, la closure est stockée sur le tas avec les variables capturées. Lorsque l'opération se termine, le système GCD ou OperationQueue place le callback dans la file d'attente appropriée (file principale ou file d'arrière-plan). Après l'exécution, le callback est supprimé de la mémoire lorsqu'il n'y a plus de références fortes. La capture list ([weak self]) empêche la rétention de l'objet après sa désallocation. Sans capture list, un retain cycle se produit, où l'objet et le callback se référencent mutuellement.
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()
}
// Utilisation avec [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)
}
}
Dans Android, le callback est passé via une interface ou une lambda. Lors de l'exécution d'une opération asynchrone via ExecutorService ou une coroutine, le callback est stocké en mémoire jusqu'à la fin du travail en arrière-plan. Les lambdas Kotlin sont compilées en classes anonymes qui capturent des variables externes. L'absence de références faibles dans la JVM nécessite une gestion manuelle : annulation du callback dans onDestroy() ou annulation des coroutines via Job.cancel(). ViewModel et LiveData résolvent ce problème au niveau du composant architectural.
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) }
}
}
}
}
// Utilisation avec lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
La syntaxe du callback est déterminée par la capacité du langage à travailler avec les fonctions comme objets de première classe. En Swift, les closures ont une syntaxe concise avec des noms d'arguments automatiques ($0, $1). En Kotlin, les lambdas supportent également it pour un seul argument. Les différences apparaissent dans le traitement de la capture de variables (capture list en Swift vs références mutables en Kotlin) et le typage (Result
Une closure Swift est un bloc de code autonome qui peut être passé et utilisé dans une autre fonction. Les closures peuvent être globales (nommées), imbriquées et au niveau expression. @escaping marque une closure qui sera exécutée après le retour de la fonction — c'est une exigence obligatoire pour les callbacks asynchrones. Sans @escaping, la closure ne peut être exécutée qu'à l'intérieur du corps de la fonction. La syntaxe trailing closure permet de passer la closure après les parenthèses : 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 supporte les fonctions d'ordre supérieur qui acceptent d'autres fonctions comme paramètres. Le callback en Kotlin est passé via un paramètre de type (T) -> Unit ou (T) -> R pour les valeurs de retour. Les fonctions suspend des coroutines Kotlin remplacent les callbacks par du code séquentiel, mais les callbacks restent dans les API compatibles Java et le SDK Android (View.setOnClickListener, TextWatcher). Les lambdas Kotlin capturent automatiquement les variables val, les variables var nécessitent des wrappers de mutabilité.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Exemple avec lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle est une situation où deux objets maintiennent des références fortes l'un sur l'autre, empêchant le gestionnaire de mémoire de les libérer. En Swift, le retain cycle se produit lorsqu'un viewController capture une closure et que la closure capture self. En Kotlin/Java, la fuite se produit lorsqu'une Activity passe une classe interne ou une lambda à une opération d'arrière-plan de longue durée. Selon la Session WWDC 10216 (2024), la gestion incorrecte des closures est la troisième cause la plus fréquente de fuites mémoire dans les applications iOS.
Swift utilise le Comptage Automatique de Références (ARC), qui libère un objet lorsque le compteur de références atteint zéro. La capture list [weak self] ou [unowned self] dans une closure empêche les retain cycles. weak self crée une référence optionnelle qui devient nil lors de la désallocation de l'objet. unowned self suppose que l'objet vit plus longtemps que la closure — la violation de cette hypothèse provoque un crash. L'utilisation de weak self comme option sécurisée par défaut est recommandée.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle !
onDataUpdate = { text in
self.process(text)
}
// Corrigé : [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
Dans Android, la fuite mémoire par callback se produit lorsqu'une Activity ou un Fragment passe un écouteur à un composant singleton (par exemple, EventBus ou Service). WeakReference permet au ramasse-miettes de libérer l'Activity même s'il existe une référence faible vers elle. Les composants lifecycle-aware (LiveData, Flow) résolvent le problème automatiquement. Les lambdas Kotlin qui capturent le contexte de l'Activity peuvent également causer des fuites : la lambda stocke implicitement une référence à 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()
}
}
}
// Utilisation dans Fragment
manager.addListener { result ->
// WeakReference ne retient pas Fragment
updateUI(result)
}
Callback Hell (également connu sous le nom de Pyramide de la Perdition) est une situation où de multiples callbacks imbriqués créent une structure de code profondément imbriquée, difficile à lire et à déboguer. Chaque étape suivante nécessite d'attendre la fin de la précédente, ce qui entraîne 5 à 10 niveaux d'imbrication. Ce problème est caractéristique des opérations asynchrones séquentielles : charger des données → analyser → sauvegarder dans la BD → mettre à jour l'UI.
Swift 5.5 a introduit les fonctions asynchrones (async/await), qui permettent d'écrire du code asynchrone de manière séquentielle. AsyncSequence et AsyncStream remplacent les itérations basées sur les callbacks. Le framework Combine fournit des opérateurs flatMap, merge, combineLatest pour la composition de flux asynchrones sans imbrication. Cependant, les callbacks restent nécessaires pour travailler avec les API Objective-C et les bibliothèques tierces sans support async.
// Callbacks imbriqués — 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 — solution
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)
}
Les coroutines Kotlin remplacent les callbacks par des fonctions suspend pour une exécution séquentielle. Flow fournit des flux cold avec des opérateurs map, flatMapConcat, combine. CoroutineScope permet d'annuler toutes les coroutines en cours lors de la destruction d'un composant. Room, Retrofit et d'autres bibliothèques Jetpack ont un support intégré pour les fonctions suspend, éliminant le besoin de callbacks dans les opérations standard.
// Callbacks séquentiels — 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 — solution
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 et Délégué sont deux approches de la notification asynchrone, et le choix dépend des exigences architecturales. Le callback est adapté aux opérations uniques avec un seul résultat. Le délégué est conçu pour des événements multiples avec différentes signatures de méthodes. Apple recommande le délégué pour les protocoles complexes avec plusieurs méthodes, et le callback pour les closures simples avec un seul résultat. Dans Android, le callback remplace le délégué dans la plupart des cas en raison du support des lambdas.
Le callback est optimal pour les opérations avec un seul résultat : requête réseau, lecture de fichier, animation avec bloc de completion. Avantages : syntaxe compacte, absence de protocole séparé, capture directe du contexte. Inconvénients : complexité avec des résultats multiples (progression, pause, annulation), impossibilité d'envoi multiple (si un callback peut être appelé plus d'une fois — utilisez un publisher).
Le délégué est adapté aux protocoles avec plusieurs méthodes obligatoires et optionnelles : UITableViewDelegate, CLLocationManagerDelegate, connexions Bluetooth. Avantages : typage clair de chaque méthode, documentation via le protocole, support des méthodes optionnelles via @objc optional. Inconvénients : code boilerplate, référence faible au délégué obligatoire (weak var delegate), complexité lors de la capture du contexte.
Questions Fréquentes
Callback est un cas particulier de fonction d'ordre supérieur. Une fonction d'ordre supérieur accepte une autre fonction comme argument ou la retourne. Un callback est une fonction passée spécifiquement pour une exécution asynchrone après l'achèvement d'une opération. Tous les callbacks sont implémentés via des fonctions d'ordre supérieur, mais toute fonction d'ordre supérieur n'est pas un callback.
Par convention, un callback doit être appelé exactement une fois — soit success, soit failure. L'appel multiple d'un même callback est considéré comme une erreur de conception. Pour des événements multiples (progression, flux de données), utilisez Observable, Publisher ou Flow — ils supportent l'émission multiple de valeurs. Certaines API violent cette règle, entraînant des bugs difficiles à trouver.
Trailing closure est un sucre syntaxique de Swift qui permet de passer une closure après les parenthèses d'appel de fonction. Si une fonction accepte une closure comme dernier argument, elle peut être placée en dehors des parenthèses : fetchData { result in ... }. Pour plusieurs closures, le trailing closure ne s'applique qu'à la dernière ; les autres sont nommées entre les parenthèses. Cela améliore la lisibilité des API basées sur les callbacks.
Utilisez WeakReference pour les écouteurs longue durée, annulez les coroutines via Job.cancel() dans onDestroy(), utilisez lifecycleScope pour l'annulation automatique. ViewModel + LiveData/Flow résout le problème au niveau architectural. Évitez de passer le contexte d'Activity aux callbacks statiques — utilisez le contexte Application. Les lambdas Kotlin capturent this implicitement, vérifiez avec un profileur mémoire.
Async/await remplace les callbacks pour le code asynchrone séquentiel, mais pas pour l'architecture pilotée par les événements. Les callbacks restent dans les API système (View.OnClickListener, délégués URLSession), les callbacks de progression et les bibliothèques tierces. Le remplacement complet est impossible en raison de la rétrocompatibilité. La stratégie moderne consiste à utiliser async/await avec des wrappers de callback (continuation en Swift, suspendCancellableCoroutine en Kotlin).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi