Callback es una función que se pasa a otra función como argumento y se ejecuta después de que finaliza una operación asíncrona. En el desarrollo móvil, el callback se utiliza para manejar los resultados de solicitudes de red, operaciones de bases de datos y animaciones. Según la Documentación de Apple (2025), los closures en Swift son la forma principal de callback y se utilizan en URLSession, GCD y Combine. En Android, el callback se implementa mediante interfaces, lambdas de Kotlin y ListenableFuture.
Puntos Clave
Callback (función de devolución de llamada) es código ejecutable que se pasa a otra función y se llama después de que se completa una acción determinada. En el desarrollo móvil, el callback es un mecanismo fundamental de la programación asíncrona, que permite reaccionar a la finalización de solicitudes de red, temporizadores, animaciones y operaciones de E/S sin bloquear el hilo principal. Swift y Kotlin proporcionan construcciones sintácticas integradas para crear callbacks: closures y lambdas respectivamente.
Una función de orden superior acepta otra función como parámetro y la llama después de ejecutar su lógica principal. El flujo de control se devuelve al llamante a través del callback, de ahí el nombre. En iOS el callback se utiliza en UIKit (animaciones UIView.animate), Foundation (URLSession.dataTask) y Combine (sink). En Android el callback se utiliza en View.OnClickListener, Retrofit Callback y Room DAO. Las API modernas reemplazan cada vez más los callbacks con async/await o corutinas, pero comprender los callbacks es necesario para trabajar con código heredado y API de bajo nivel.
El callback puede ser síncrono (se llama inmediatamente dentro de la función) y asíncrono (se llama más tarde desde otro hilo o cola). Los callbacks síncronos se utilizan para ordenación (comparadores) y recorrido de colecciones. Los callbacks asíncronos se utilizan para solicitudes de red, lectura de archivos y trabajo con sensores. La diferencia es crítica para comprender el threading: el callback síncrono se ejecuta en el mismo hilo, el asíncrono en un hilo determinado por el despachador (DispatchQueue en iOS, Dispatchers en Kotlin).
El mecanismo de callback en ambas plataformas se basa en el mismo principio: una función se pasa como objeto de primera clase y se almacena hasta el momento de la ejecución. Sin embargo, las implementaciones difieren debido a diferentes paradigmas de lenguaje. En iOS, un callback es un closure que captura variables del contexto circundante. En Android, un callback se implementa más a menudo mediante clases anónimas o expresiones lambda de Kotlin, compiladas en FunctionalInterface.
Al llamar a una función asíncrona, el closure se almacena en el heap junto con las variables capturadas. Cuando la operación se completa, el sistema GCD u OperationQueue coloca el callback en la cola correspondiente (cola principal o cola en segundo plano). Después de la ejecución, el callback se elimina de la memoria cuando no hay referencias fuertes. La capture list ([weak self]) evita que el objeto se retenga después de su desasignación. Sin capture list se produce un retain cycle, donde el objeto y el callback se referencian mutuamente.
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)
}
}
En Android, el callback se pasa a través de una interfaz o lambda. Al ejecutar una operación asíncrona mediante ExecutorService o una corutina, el callback se almacena en memoria hasta que se completa el trabajo en segundo plano. Las lambdas de Kotlin se compilan en clases anónimas que capturan variables externas. La ausencia de referencias débiles en la JVM requiere gestión manual: anular el callback en onDestroy() o cancelar corutinas mediante Job.cancel(). ViewModel y LiveData resuelven este problema a nivel de componente arquitectónico.
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) }
})
La sintaxis del callback está determinada por las capacidades del lenguaje para trabajar con funciones como objetos de primera clase. En Swift, los closures tienen una sintaxis concisa con nombres de argumentos automáticos ($0, $1). En Kotlin, las lambdas también admiten it para un único argumento. Las diferencias se manifiestan en el manejo de la captura de variables (capture list en Swift frente a referencias mutables en Kotlin) y la tipificación (Result
Un closure de Swift es un bloque de código autónomo que puede pasarse y utilizarse en otra función. Los closures pueden ser globales (con nombre), anidados y a nivel de expresión. @escaping marca un closure que se ejecutará después de que la función retorne — este es un requisito obligatorio para callbacks asíncronos. Sin @escaping, el closure solo puede ejecutarse dentro del cuerpo de la función. La sintaxis trailing closure permite pasar el closure después de los paréntesis: 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 admite funciones de orden superior que aceptan otras funciones como parámetros. El callback en Kotlin se pasa a través de un parámetro de tipo (T) -> Unit o (T) -> R para valores de retorno. Las funciones suspend de las corutinas de Kotlin reemplazan los callbacks con código secuencial, pero los callbacks permanecen en las API compatibles con Java y en el SDK de Android (View.setOnClickListener, TextWatcher). Las lambdas de Kotlin capturan automáticamente las variables val, las variables var requieren envoltorios de mutabilidad.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Ejemplo con lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle es una situación en la que dos objetos mantienen referencias fuertes entre sí, impidiendo que el gestor de memoria los libere. En Swift, el retain cycle ocurre cuando un viewController captura un closure y el closure captura self. En Kotlin/Java, la fuga ocurre cuando una Activity pasa una clase interna o lambda a una operación en segundo plano de larga duración. Según la Sesión 10216 de la WWDC (2024), la gestión incorrecta de closures es la tercera causa más frecuente de fugas de memoria en aplicaciones iOS.
Swift utiliza Automatic Reference Counting (ARC), que libera un objeto cuando el contador de referencias llega a cero. La capture list [weak self] o [unowned self] en un closure evita retain cycles. weak self crea una referencia opcional que se vuelve nil cuando el objeto se desasigna. unowned self asume que el objeto vive más que el closure — si se viola esta suposición, se produce un crash. Se recomienda usar weak self como opción segura por defecto.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Corregido: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
En Android, la fuga de memoria por callback ocurre cuando una Activity o Fragment pasa un listener a un componente singleton (por ejemplo, EventBus o Service). WeakReference permite al recolector de basura liberar la Activity incluso si hay una referencia débil a ella. Los componentes lifecycle-aware (LiveData, Flow) resuelven el problema automáticamente. Las lambdas de Kotlin que capturan el contexto de la Activity también pueden causar fugas: la lambda almacena implícitamente una referencia a 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()
}
}
}
// Uso en Fragment
manager.addListener { result ->
// WeakReference no retiene Fragment
updateUI(result)
}
Callback Hell (también conocido como Pyramid of Doom) es una situación en la que múltiples callbacks anidados crean una estructura de código profundamente anidada, difícil de leer y depurar. Cada paso siguiente requiere esperar la finalización del anterior, lo que lleva a una anidación de 5-10 niveles. Este problema es característico de las operaciones asíncronas secuenciales: carga de datos → análisis → guardado en BD → actualización de UI.
Swift 5.5 introdujo funciones asíncronas (async/await), que permiten escribir código asíncrono de forma secuencial. AsyncSequence y AsyncStream reemplazan las iteraciones basadas en callbacks. El framework Combine proporciona operadores flatMap, merge, combineLatest para la composición de flujos asíncronos sin anidación. Sin embargo, los callbacks siguen siendo necesarios para trabajar con API Objective-C y bibliotecas de terceros sin soporte async.
// Callbacks anidados — 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 — solución
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)
}
Las corutinas de Kotlin reemplazan los callbacks con funciones suspend para ejecución secuencial. Flow proporciona flujos cold con operadores map, flatMapConcat, combine. CoroutineScope permite cancelar todas las corutinas en ejecución cuando se destruye un componente. Room, Retrofit y otras bibliotecas de Jetpack tienen soporte integrado para funciones suspend, eliminando la necesidad de callbacks en operaciones estándar.
// Callbacks secuenciales — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Corutinas — solución
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 y Delegate son dos enfoques para la notificación asíncrona, y la elección depende de los requisitos arquitectónicos. El callback es adecuado para operaciones únicas con un solo resultado. El delegate está diseñado para eventos múltiples con diferentes firmas de métodos. Apple recomienda delegate para protocolos complejos con varios métodos, y callback para closures simples con un solo resultado. En Android, el callback reemplaza al delegate en la mayoría de los casos debido al soporte de lambdas.
El callback es óptimo para operaciones con un único resultado: solicitud de red, lectura de archivo, animación con bloque de finalización. Ventajas: sintaxis compacta, ausencia de protocolo separado, captura directa del contexto. Desventajas: complejidad con resultados múltiples (progreso, pausa, cancelación), imposibilidad de envío múltiple (si un callback puede llamarse más de una vez, use un publisher).
El delegate es adecuado para protocolos con varios métodos obligatorios y opcionales: UITableViewDelegate, CLLocationManagerDelegate, conexiones Bluetooth. Ventajas: tipificación clara de cada método, documentación a través del protocolo, soporte de métodos opcionales mediante @objc optional. Desventajas: código boilerplate, referencia débil al delegate obligatoria (weak var delegate), complejidad al capturar el contexto.
Preguntas Frecuentes
Callback es un caso particular de función de orden superior. Una función de orden superior acepta otra función como argumento o la devuelve. Un callback es una función pasada específicamente para ejecución asíncrona después de completar una operación. Todos los callbacks se implementan mediante funciones de orden superior, pero no toda función de orden superior es un callback.
Por convención, un callback debe llamarse exactamente una vez — ya sea success o failure. La llamada múltiple de un mismo callback se considera un error de diseño. Para eventos múltiples (progreso, flujo de datos), use Observable, Publisher o Flow — admiten emisión múltiple de valores. Algunas API violan esta regla, lo que conduce a errores difíciles de encontrar.
Trailing closure es azúcar sintáctica de Swift que permite pasar un closure después de los paréntesis de llamada a la función. Si una función acepta un closure como último argumento, se puede colocar fuera de los paréntesis: fetchData { result in ... }. Para múltiples closures, trailing closure se aplica solo al último; los restantes se nombran dentro de los paréntesis. Esto mejora la legibilidad de las API basadas en callbacks.
Use WeakReference para listeners de larga duración, cancele corutinas mediante Job.cancel() en onDestroy(), use lifecycleScope para cancelación automática. ViewModel + LiveData/Flow resuelve el problema a nivel arquitectónico. Evite pasar el contexto de Activity a callbacks estáticos — use Application context. Las lambdas de Kotlin capturan this implícitamente, verifique con un memory profiler.
Async/await reemplaza los callbacks para código asíncrono secuencial, pero no para arquitectura basada en eventos. Los callbacks permanecen en API del sistema (View.OnClickListener, delegados de URLSession), callbacks de progreso y bibliotecas de terceros. El reemplazo completo es imposible debido a la compatibilidad hacia atrás. La estrategia moderna es usar async/await con envoltorios de callback (continuation en Swift, suspendCancellableCoroutine en Kotlin).
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también