Callback — este o funcție transmisă unei alte funcții ca argument și executată după finalizarea unei operații asincrone. În dezvoltarea mobilă, callback-ul este utilizat pentru procesarea rezultatelor cererilor de rețea, lucrul cu baze de date și animații. Conform Apple Documentation (2025), closure-urile în Swift sunt forma principală de callback și sunt utilizate în URLSession, GCD și Combine. În Android, callback-ul este implementat prin interfețe, lambde Kotlin și ListenableFuture.
Principalele
Callback (funcție de revenire) — este un cod executabil transmis unei alte funcții și apelat după finalizarea unei acțiuni specifice. În dezvoltarea mobilă, callback-ul este mecanismul fundamental al programării asincrone, permițând reacția la finalizarea cererilor de rețea, timerelor, animațiilor și operațiilor de intrare/ieșire fără blocarea firului principal. Swift și Kotlin oferă construcții sintactice încorporate pentru crearea callback-urilor — closure-uri și, respectiv, lambde.
O funcție de ordin superior acceptă o altă funcție ca parametru și o apelează după executarea logicii sale principale. Fluxul de control revine apelantului prin callback, de unde și numele. În iOS, callback-ul se aplică în UIKit (animații UIView.animate), Foundation (URLSession.dataTask) și Combine (sink). În Android, callback-ul este utilizat în View.OnClickListener, Retrofit Callback și Room DAO. API-urile moderne înlocuiesc tot mai mult callback-ul cu async/await sau corutine, dar înțelegerea callback-ului este necesară pentru lucrul cu cod moștenit și API-uri de nivel scăzut.
Callback-ul poate fi sincron (apelat imediat în interiorul funcției) și asincron (apelat mai târziu dintr-un alt fir sau coadă). Callback-urile sincrone sunt utilizate pentru sortare (comparatori) și parcurgerea colecțiilor. Callback-urile asincrone sunt aplicate pentru cereri de rețea, citire fișiere și lucru cu senzori. Diferența este critică pentru înțelegerea threading-ului: callback-ul sincron se execută în același fir, cel asincron — în firul determinat de dispecer (DispatchQueue în iOS, Dispatchers în Kotlin).
Mecanismul callback pe ambele platforme se bazează pe același principiu: funcția este transmisă ca obiect de primă clasă și stocată până la momentul executării. Cu toate acestea, implementările diferă din cauza paradigmelor lingvistice diferite. În iOS, callback-ul este un closure care capturează variabile din contextul înconjurător. În Android, callback-ul este cel mai adesea implementat prin clase anonime sau expresii lambda Kotlin, compilate în FunctionalInterface.
La apelarea unei funcții asincrone, closure-ul este salvat în heap împreună cu variabilele capturate. Când operația se finalizează, sistemul GCD sau OperationQueue plasează callback-ul în coada corespunzătoare (main queue sau background queue). După execuție, callback-ul este eliminat din memorie în absența referințelor puternice. Capture list ([weak self]) previne menținerea obiectului după dealocarea acestuia. Fără capture list, apare un retain cycle, în care obiectul și callback-ul se referă reciproc unul la celălalt.
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()
}
// Utilizare cu [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)
}
}
În Android, callback-ul este transmis printr-o interfață sau o lambdă. La executarea unei operații asincrone prin ExecutorService sau o corutină, callback-ul este stocat în memorie până la finalizarea lucrului în fundal. Lambdele Kotlin sunt compilate în clase anonime care capturează variabile externe. Absența referințelor slabe în JVM necesită gestionare manuală: resetarea callback-ului în onDestroy() sau anularea corutinelor prin Job.cancel(). ViewModel și LiveData rezolvă această problemă la nivel de componentă arhitecturală.
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) }
}
}
}
}
// Utilizare cu lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
Sintaxa callback-ului este determinată de capacitățile lingvistice de lucru cu funcții ca obiecte de primă clasă. În Swift, closure-urile au o sintaxă concisă cu nume automate de argumente ($0, $1). În Kotlin, lambdele acceptă de asemenea it pentru un singur argument. Diferențele se manifestă în gestionarea capturării variabilelor (capture list în Swift vs referințe mutabile în Kotlin) și tipizare (Result<Success, Failure> vs Result<T>).
Un closure în Swift este un bloc de cod autosuficient care poate fi transmis și utilizat într-o altă funcție. Closure-urile pot fi globale (denumite), imbricate și expression-level. @escaping marchează un closure care va fi executat după revenirea din funcție — aceasta este o cerință obligatorie pentru callback-urile asincrone. Fără @escaping, closure-ul poate fi executat doar în interiorul corpului funcției. Sintaxa trailing closure permite transmiterea closure-ului după parantezele rotunde: 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 acceptă funcții de ordin superior care primesc alte funcții ca parametri. Callback-ul în Kotlin este transmis printr-un parametru de tip (T) -> Unit sau (T) -> R pentru valoarea returnată. Funcțiile suspend ale corutinelor Kotlin înlocuiesc callback-ul cu cod secvențial, dar callback-ul rămâne în API-urile compatibile Java și Android SDK (View.setOnClickListener, TextWatcher). Lambdele Kotlin capturează automat variabilele val, variabilele var necesită învelișuri mutabile.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Exemplu cu lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — situația în care două obiecte mențin referințe puternice unul asupra celuilalt, împiedicând eliberarea lor de către colectorul de memorie. În Swift, retain cycle apare atunci când viewController capturează un closure, iar closure-ul capturează self. În Kotlin/Java, scurgerea apare atunci când Activity transmite o clasă internă sau o lambdă într-o operație lungă de fundal. Conform WWDC Session 10216 (2024), gestionarea incorectă a closure-urilor este a treia cea mai frecventă cauză de scurgeri de memorie în aplicațiile iOS.
Swift utilizează Automatic Reference Counting (ARC), care eliberează obiectul la resetarea contorului de referințe. Capture list [weak self] sau [unowned self] în closure previne retain cycle. weak self creează o referință opțională care devine nil la dealocarea obiectului. unowned self presupune că obiectul trăiește mai mult decât closure-ul — încălcarea acestei presupuneri duce la crash. Se recomandă utilizarea weak self ca opțiune sigură implicită.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Corectat: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
În Android, scurgerea de callback apare atunci când Activity sau Fragment transmite un ascultător unui component singleton (de exemplu, EventBus sau Service). WeakReference permite colectorului de gunoi să elibereze Activity, chiar dacă există o referință slabă asupra sa. Componentele Lifecycle-aware (LiveData, Flow) rezolvă problema automat. Lambdele Kotlin care capturează contextul Activity pot provoca, de asemenea, scurgeri: lambda păstrează implicit o referință la 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()
}
}
}
// Utilizare în Fragment
manager.addListener { result ->
// WeakReference nu reține Fragment
updateUI(result)
}
Callback Hell (cunoscut și sub numele de Piramida Morții) — este situația în care numeroase callback-uri imbricate creează o structură de cod profund imbricată, dificil de citit și depanat. Fiecare pas următor necesită așteptarea finalizării celui precedent, ceea ce duce la imbricare de 5-10 niveluri. Această problemă este caracteristică operațiilor asincrone secvențiale: încărcare date → parsare → salvare în bază de date → actualizare UI.
Swift 5.5 a introdus funcții asincrone (async/await) care permit scrierea codului asincron secvențial. AsyncSequence și AsyncStream înlocuiesc iterațiile bazate pe callback. Framework-ul Combine oferă operatori flatMap, merge, combineLatest pentru compunerea fluxurilor asincrone fără imbricare. Cu toate acestea, callback-ul rămâne necesar pentru lucrul cu API-uri Objective-C și biblioteci terțe fără suport async.
// Callback-uri imbricate — 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 — soluție
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)
}
Corutinele Kotlin înlocuiesc callback-ul cu funcții suspend cu execuție secvențială. Flow oferă fluxuri cold cu operatorii map, flatMapConcat, combine. CoroutineScope permite anularea tuturor corutinelor lansate la distrugerea componentei. Room, Retrofit și alte biblioteci Jetpack au suport încorporat pentru funcții suspend, eliminând necesitatea callback-ului pentru operații standard.
// Callback-uri secvențiale — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Corutine — soluție
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 și Delegate — două abordări pentru notificarea asincronă, iar alegerea între ele depinde de cerințele arhitecturale. Callback-ul este potrivit pentru operații unice cu un singur rezultat. Delegate-ul este destinat evenimentelor multiple cu semnături diferite de metode. Apple recomandă delegate-ul pentru protocoale complexe cu mai multe metode, callback-ul — pentru closure-uri simple cu un singur rezultat. În Android, callback-ul înlocuiește delegate-ul în majoritatea cazurilor datorită suportului pentru lambde.
Callback-ul este optim pentru operații cu un singur rezultat: cerere de rețea, citire fișier, animație cu bloc de finalizare. Avantaje: sintaxă compactă, lipsa unui protocol separat, capturarea directă a contextului. Dezavantaje: complexitate la rezultate multiple (progres, pauză, anulare), imposibilitatea trimiterii multiple (dacă callback-ul poate fi apelat de mai multe ori — utilizați un publisher).
Delegate-ul este potrivit pentru protocoale cu mai multe metode obligatorii și opționale: UITableViewDelegate, CLLocationManagerDelegate, conexiuni Bluetooth. Avantaje: tipizare clară a fiecărei metode, documentare prin protocol, suport pentru metode opționale prin @objc optional. Dezavantaje: cod boilerplate, referință slabă obligatorie la delegate (weak var delegate), complexitate la capturarea contextului.
Întrebări frecvente
Callback este un caz particular al funcției de ordin superior. O funcție de ordin superior primește o altă funcție ca argument sau o returnează. Callback-ul este o funcție transmisă special pentru execuția asincronă după finalizarea operației. Toate callback-urile sunt implementate prin funcții de ordin superior, dar nu orice funcție de ordin superior este un callback.
Prin convenție, callback-ul trebuie apelat exact o dată — fie success, fie failure. Apelarea multiplă a aceluiași callback este considerată o eroare de proiectare. Pentru evenimente multiple (progres, flux de date) utilizați Observable, Publisher sau Flow — acestea acceptă emisia multiplă de valori. Unele API-uri încalcă această regulă, ceea ce duce la bug-uri greu de depistat.
Trailing closure — zahăr sintactic în Swift care permite transmiterea unui closure după parantezele rotunde ale apelului de funcție. Dacă funcția acceptă un closure ca ultim argument, acesta poate fi plasat în afara parantezelor: fetchData { result in ... }. Pentru mai multe closure-uri, trailing closure se aplică doar ultimului, celelalte fiind denumite în interiorul parantezelor. Acest lucru îmbunătățește lizibilitatea API-urilor bazate pe callback.
Utilizați WeakReference pentru ascultători de lungă durată, anulați corutinele prin Job.cancel() în onDestroy(), aplicați lifecycleScope pentru anulare automată. ViewModel + LiveData/Flow rezolvă problema la nivel arhitectural. Evitați transmiterea contextului Activity în callback-uri statice — utilizați contextul aplicației. Lambdele Kotlin capturează implicit this, verificați prin memory profiler.
Async/await înlocuiește callback-ul pentru cod asincron secvențial, dar nu pentru arhitectura event-driven. Callback-ul rămâne în API-urile de sistem (View.OnClickListener, delegați URLSession), apeluri cu progres și biblioteci terțe. Înlocuirea completă este imposibilă din cauza compatibilității inverse. Strategia modernă este utilizarea async/await cu învelișuri de callback (continuation în Swift, suspendCancellableCoroutine în Kotlin).
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și