Callback — cos'è, funzioni di callback e come funzionano

Autore: IT Sectr Pubblicato: 2026-03-17 Tempo di lettura: 11 min

Callback è una funzione che viene passata a un'altra funzione come argomento e viene eseguita dopo il completamento di un'operazione asincrona. Nello sviluppo mobile, il callback viene utilizzato per elaborare i risultati di richieste di rete, operazioni su database e animazioni. Secondo la Documentazione Apple (2025), le closure in Swift sono la forma principale di callback e vengono utilizzate in URLSession, GCD e Combine. In Android, il callback è implementato tramite interfacce, lambda Kotlin e ListenableFuture.

Punti Chiave

  • Callback — funzione di callback passata come argomento per l'esecuzione asincrona.
  • Swift utilizza le closure con la parola chiave @escaping per i callback.
  • Kotlin applica espressioni lambda e funzioni di ordine superiore per i callback.
  • Retain cycle — perdita di memoria quando si cattura self in un callback su iOS.
  • Callback Hell — il problema dei callback annidati, risolto con async/await e coroutine.

Cos'è un Callback?

Callback (funzione di callback) è codice eseguibile che viene passato a un'altra funzione e chiamato dopo il completamento di un'azione specifica. Nello sviluppo mobile, il callback è un meccanismo fondamentale della programmazione asincrona, che consente di reagire al completamento di richieste di rete, timer, animazioni e operazioni di I/O senza bloccare il thread principale. Swift e Kotlin forniscono costruzioni sintattiche integrate per creare callback — rispettivamente closure e lambda.

Principio di funzionamento del Callback

Una funzione di ordine superiore accetta un'altra funzione come parametro e la chiama dopo aver eseguito la sua logica principale. Il flusso di controllo viene restituito al chiamante tramite il callback, da cui il nome. In iOS, il callback viene utilizzato in UIKit (animazioni UIView.animate), Foundation (URLSession.dataTask) e Combine (sink). In Android, il callback viene utilizzato in View.OnClickListener, Retrofit Callback e Room DAO. Le API moderne stanno sempre più sostituendo i callback con async/await o coroutine, ma comprendere i callback è necessario per lavorare con codice legacy e API di basso livello.

Callback sincrono e asincrono

Il callback può essere sincrono (chiamato immediatamente all'interno della funzione) e asincrono (chiamato successivamente da un altro thread o coda). I callback sincroni vengono utilizzati per l'ordinamento (comparatori) e l'attraversamento di collezioni. I callback asincroni vengono utilizzati per richieste di rete, lettura di file e lavoro con sensori. La differenza è critica per comprendere il threading: il callback sincrono viene eseguito nello stesso thread, quello asincrono in un thread determinato dal dispatcher (DispatchQueue in iOS, Dispatchers in Kotlin).

Come funziona il Callback in iOS e Android?

Il meccanismo di callback su entrambe le piattaforme si basa sullo stesso principio: una funzione viene passata come oggetto di prima classe e memorizzata fino al momento dell'esecuzione. Tuttavia, le implementazioni differiscono a causa di diversi paradigmi linguistici. In iOS, un callback è una closure che cattura variabili dal contesto circostante. In Android, un callback è più spesso implementato tramite classi anonime o espressioni lambda Kotlin, compilate in FunctionalInterface.

Ciclo di vita del Callback in iOS

Quando viene chiamata una funzione asincrona, la closure viene memorizzata nell'heap insieme alle variabili catturate. Quando l'operazione viene completata, il sistema GCD o OperationQueue inserisce il callback nella coda appropriata (coda principale o coda in background). Dopo l'esecuzione, il callback viene rimosso dalla memoria quando non ci sono riferimenti forti. La capture list ([weak self]) impedisce che l'oggetto venga trattenuto dopo la sua deallocazione. Senza capture list, si verifica un retain cycle, in cui l'oggetto e il callback si riferiscono l'un l'altro.

swift
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()
}

// Uso con [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)
    }
}

Ciclo di vita del Callback in Android

In Android, il callback viene passato tramite un'interfaccia o una lambda. Quando si esegue un'operazione asincrona tramite ExecutorService o una coroutine, il callback viene memorizzato in memoria fino al completamento del lavoro in background. Le lambda Kotlin vengono compilate in classi anonime che catturano variabili esterne. L'assenza di riferimenti deboli nella JVM richiede una gestione manuale: annullare il callback in onDestroy() o cancellare le coroutine tramite Job.cancel(). ViewModel e LiveData risolvono questo problema a livello di componente architetturale.

kotlin
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) }
            }
        }
    }
}

// Uso con lambda
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

Sintassi del Callback in Swift e Kotlin

La sintassi del callback è determinata dalla capacità del linguaggio di lavorare con le funzioni come oggetti di prima classe. In Swift, le closure hanno una sintassi concisa con nomi di argomenti automatici ($0, $1). In Kotlin, le lambda supportano anche it per un singolo argomento. Le differenze si manifestano nella gestione della cattura di variabili (capture list in Swift vs riferimenti mutabili in Kotlin) e nella tipizzazione (Result vs Result).

Callback in Swift: closure

Una closure Swift è un blocco di codice autonomo che può essere passato e utilizzato in un'altra funzione. Le closure possono essere globali (nominate), annidate e a livello di espressione. @escaping marca una closure che verrà eseguita dopo il ritorno della funzione — questo è un requisito obbligatorio per i callback asincroni. Senza @escaping, la closure può essere eseguita solo all'interno del corpo della funzione. La sintassi trailing closure consente di passare la closure dopo le parentesi: fetchData { result in ... }.

swift
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)
    }
}

Callback in Kotlin: lambda e funzioni di ordine superiore

Kotlin supporta funzioni di ordine superiore che accettano altre funzioni come parametri. Il callback in Kotlin viene passato tramite un parametro di tipo (T) -> Unit o (T) -> R per valori di ritorno. Le funzioni suspend delle coroutine Kotlin sostituiscono i callback con codice sequenziale, ma i callback rimangono nelle API compatibili con Java e nell'SDK Android (View.setOnClickListener, TextWatcher). Le lambda Kotlin catturano automaticamente le variabili val, le variabili var richiedono wrapper di mutabilità.

kotlin
fun <T, R> processWithCallback(
    input: T,
    transform: (T) -> R,
    onResult: (R) -> Unit
) {
    thread {
        val result = transform(input)
        runOnUiThread { onResult(result) }
    }
}

// Esempio con lambda
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cycles e perdite di memoria nel Callback

Retain cycle è una situazione in cui due oggetti mantengono riferimenti forti l'uno all'altro, impedendo al gestore della memoria di rilasciarli. In Swift, il retain cycle si verifica quando un viewController cattura una closure e la closure cattura self. In Kotlin/Java, la perdita si verifica quando un'Activity passa una classe interna o una lambda a un'operazione in background di lunga durata. Secondo la Sessione WWDC 10216 (2024), la gestione impropria delle closure è la terza causa più comune di perdite di memoria nelle applicazioni iOS.

Retain cycles in Swift

Swift utilizza il Conteggio Automatico dei Riferimenti (ARC), che rilascia un oggetto quando il contatore dei riferimenti raggiunge lo zero. La capture list [weak self] o [unowned self] in una closure previene i retain cycle. weak self crea un riferimento opzionale che diventa nil quando l'oggetto viene deallocato. unowned self presuppone che l'oggetto viva più a lungo della closure — la violazione di questa ipotesi provoca un crash. Si consiglia di utilizzare weak self come opzione sicura predefinita.

swift
class DataController {
    var onDataUpdate: ((String) -> Void)?

    func setupCallback() {
        // Retain cycle!
        onDataUpdate = { text in
            self.process(text)
        }

        // Corretto: [weak self]
        onDataUpdate = { [weak self] text in
            guard let self else { return }
            self.process(text)
        }
    }

    func process(_ input: String) { }
}

Perdite di memoria in Android

In Android, la perdita di memoria da callback si verifica quando un'Activity o un Fragment passa un listener a un componente singleton (ad esempio, EventBus o Service). WeakReference consente al garbage collector di rilasciare l'Activity anche se esiste un riferimento debole ad essa. I componenti lifecycle-aware (LiveData, Flow) risolvono automaticamente il problema. Le lambda Kotlin che catturano il contesto dell'Activity possono anche causare perdite: la lambda memorizza implicitamente un riferimento a this.

kotlin
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()
        }
    }
}

// Uso in Fragment
manager.addListener { result ->
    // WeakReference non trattiene Fragment
    updateUI(result)
}

Callback Hell e modi per affrontarlo

Callback Hell (noto anche come Piramide della Perdizione) è una situazione in cui molteplici callback annidati creano una struttura di codice profondamente annidata, difficile da leggere e debug. Ogni passo successivo richiede di attendere il completamento del precedente, portando a 5-10 livelli di annidamento. Questo problema è caratteristico delle operazioni asincrone sequenziali: caricare dati → analizzare → salvare nel DB → aggiornare l'UI.

Soluzioni in Swift: async/await

Swift 5.5 ha introdotto le funzioni asincrone (async/await), che consentono di scrivere codice asincrono in modo sequenziale. AsyncSequence e AsyncStream sostituiscono le iterazioni basate su callback. Il framework Combine fornisce operatori flatMap, merge, combineLatest per la composizione di flussi asincroni senza annidamento. Tuttavia, i callback rimangono necessari per lavorare con API Objective-C e librerie di terze parti senza supporto async.

swift
// Callback annidati — 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 — soluzione
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)
}

Soluzioni in Kotlin: coroutine e Flow

Le coroutine Kotlin sostituiscono i callback con funzioni suspend per l'esecuzione sequenziale. Flow fornisce flussi cold con operatori map, flatMapConcat, combine. CoroutineScope consente di cancellare tutte le coroutine in esecuzione quando un componente viene distrutto. Room, Retrofit e altre librerie Jetpack hanno supporto integrato per le funzioni suspend, eliminando la necessità di callback nelle operazioni standard.

kotlin
// Callback sequenziali — Callback Hell
api.login(credentials) { user ->
    api.fetchProfile(user.id) { profile ->
        api.download(profile.avatarUrl) { bytes ->
            file.save(bytes) { result ->
                textView.text = result.toString()
            }
        }
    }
}

// Coroutine — soluzione
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 vs Delegato: cosa scegliere?

Callback e Delegato sono due approcci alla notifica asincrona e la scelta dipende dai requisiti architetturali. Il callback è adatto per operazioni singole con un unico risultato. Il delegato è progettato per eventi multipli con diverse firme di metodi. Apple raccomanda il delegato per protocolli complessi con più metodi e il callback per closure semplici con un unico risultato. In Android, il callback sostituisce il delegato nella maggior parte dei casi grazie al supporto delle lambda.

Quando scegliere il Callback

Il callback è ottimale per operazioni con un unico risultato: richiesta di rete, lettura di file, animazione con blocco di completamento. Vantaggi: sintassi compatta, assenza di protocollo separato, cattura diretta del contesto. Svantaggi: complessità con risultati multipli (progresso, pausa, annullamento), impossibilità di invio multiplo (se un callback può essere chiamato più di una volta — utilizzare un publisher).

Quando scegliere il Delegato

Il delegato è adatto per protocolli con più metodi obbligatori e opzionali: UITableViewDelegate, CLLocationManagerDelegate, connessioni Bluetooth. Vantaggi: tipizzazione chiara di ogni metodo, documentazione tramite il protocollo, supporto di metodi opzionali tramite @objc optional. Svantaggi: codice boilerplate, riferimento debole al delegato obbligatorio (weak var delegate), complessità nella cattura del contesto.

Domande Frequenti

Qual è la differenza tra callback e funzione di ordine superiore?

Callback è un caso particolare di funzione di ordine superiore. Una funzione di ordine superiore accetta un'altra funzione come argomento o la restituisce. Un callback è una funzione passata specificamente per l'esecuzione asincrona dopo il completamento di un'operazione. Tutti i callback sono implementati tramite funzioni di ordine superiore, ma non ogni funzione di ordine superiore è un callback.

Un callback può essere chiamato più volte?

Per convenzione, un callback dovrebbe essere chiamato esattamente una volta — o success o failure. La chiamata multipla dello stesso callback è considerata un errore di progettazione. Per eventi multipli (progresso, flusso di dati), utilizzare Observable, Publisher o Flow — supportano l'emissione multipla di valori. Alcune API violano questa regola, portando a bug difficili da trovare.

Cos'è il trailing closure in Swift?

Trailing closure è zucchero sintattico di Swift che consente di passare una closure dopo le parentesi tonde della chiamata di funzione. Se una funzione accetta una closure come ultimo argomento, può essere posizionata fuori dalle parentesi: fetchData { result in ... }. Per più closure, il trailing closure si applica solo all'ultima; le rimanenti vengono nominate tra le parentesi. Questo migliora la leggibilità delle API basate su callback.

Come evitare perdite di memoria con i callback in Android?

Utilizzare WeakReference per listener di lunga durata, cancellare le coroutine tramite Job.cancel() in onDestroy(), utilizzare lifecycleScope per la cancellazione automatica. ViewModel + LiveData/Flow risolve il problema a livello architetturale. Evitare di passare il contesto dell'Activity a callback statici — utilizzare il contesto Application. Le lambda Kotlin catturano this implicitamente, verificare con un memory profiler.

Async/await sostituirà completamente i callback?

Async/await sostituisce i callback per il codice asincrono sequenziale, ma non per l'architettura guidata dagli eventi. I callback rimangono nelle API di sistema (View.OnClickListener, delegati URLSession), callback di progresso e librerie di terze parti. La sostituzione completa è impossibile a causa della retrocompatibilità. La strategia moderna è utilizzare async/await con wrapper di callback (continuation in Swift, suspendCancellableCoroutine in Kotlin).

Riepilogo

  • Callback — funzione di callback passata come argomento per l'esecuzione asincrona dopo il completamento di un'operazione.
  • Swift implementa i callback tramite closure con @escaping, capture list [weak self] e sintassi trailing closure.
  • Kotlin utilizza lambda, funzioni di ordine superiore e funzioni suspend delle coroutine per l'asincronia.
  • Retain cycle in iOS viene prevenuto con la capture list; in Android — con WeakReference e componenti lifecycle-aware.
  • Callback Hell viene risolto con async/await in Swift e coroutine con Flow in Kotlin.
  • Delegato è preferibile al callback per protocolli con più metodi; callback per operazioni singole.
  • Utilizzare callback per operazioni asincrone semplici, async/await per catene sequenziali, delegato per eventi multipli.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche