Callback — είναι μια συνάρτηση που μεταβιβάζεται σε μια άλλη συνάρτηση ως όρισμα και εκτελείται μετά την ολοκλήρωση μιας ασύγχρονης λειτουργίας. Στην ανάπτυξη εφαρμογών για κινητά, το callback χρησιμοποιείται για την επεξεργασία αποτελεσμάτων αιτημάτων δικτύου, εργασία με βάσεις δεδομένων και κινούμενα σχέδια. Σύμφωνα με το Apple Documentation (2025), τα closures στη Swift είναι η βασική μορφή callback και χρησιμοποιούνται σε URLSession, GCD και Combine. Στο Android, το callback υλοποιείται μέσω διεπαφών, λάμδα Kotlin και ListenableFuture.
Κύρια σημεία
Callback (συνάρτηση επιστροφής κλήσης) — είναι εκτελέσιμος κώδικας που μεταβιβάζεται σε μια άλλη συνάρτηση και καλείται μετά την ολοκλήρωση μιας συγκεκριμένης ενέργειας. Στην ανάπτυξη εφαρμογών για κινητά, το callback είναι ο θεμελιώδης μηχανισμός ασύγχρονου προγραμματισμού, επιτρέποντας την αντίδραση στην ολοκλήρωση αιτημάτων δικτύου, χρονοδιακοπτών, κινούμενων σχεδίων και λειτουργιών εισόδου/εξόδου χωρίς αποκλεισμό του κύριου νήματος. Η Swift και η Kotlin παρέχουν ενσωματωμένες συντακτικές κατασκευές για τη δημιουργία callback — closures και λάμδα αντίστοιχα.
Μια συνάρτηση ανώτερης τάξης δέχεται μια άλλη συνάρτηση ως παράμετρο και την καλεί μετά την εκτέλεση της κύριας λογικής της. Η ροή ελέγχου επιστρέφει στον καλούντα μέσω του callback, εξού και η ονομασία. Στο iOS, το callback εφαρμόζεται στο UIKit (κινούμενα σχέδια UIView.animate), Foundation (URLSession.dataTask) και Combine (sink). Στο Android, το callback χρησιμοποιείται σε View.OnClickListener, Retrofit Callback και Room DAO. Τα σύγχρονα API αντικαθιστούν όλο και περισσότερο το callback με async/await ή coroutine, αλλά η κατανόηση του callback είναι απαραίτητη για εργασία με παλαιό κώδικα και API χαμηλού επιπέδου.
Το callback μπορεί να είναι σύγχρονο (καλείται αμέσως εντός της συνάρτησης) και ασύγχρονο (καλείται αργότερα από άλλο νήμα ή ουρά). Τα σύγχρονα callback χρησιμοποιούνται για ταξινόμηση (συγκριτές) και διάσχιση συλλογών. Ασύγχρονα callback εφαρμόζονται για αιτήματα δικτύου, ανάγνωση αρχείων και εργασία με αισθητήρες. Η διαφορά είναι κρίσιμη για την κατανόηση threading: το σύγχρονο callback εκτελείται στο ίδιο νήμα, το ασύγχρονο — στο νήμα που καθορίζεται από τον διανομέα (DispatchQueue στο iOS, Dispatchers στο Kotlin).
Ο μηχανισμός callback και στις δύο πλατφόρμες βασίζεται στην ίδια αρχή: η συνάρτηση μεταβιβάζεται ως αντικείμενο πρώτης τάξης και αποθηκεύεται μέχρι τη στιγμή εκτέλεσης. Ωστόσο, οι υλοποιήσεις διαφέρουν λόγω διαφορετικών γλωσσικών παραδειγμάτων. Στο iOS, το callback είναι ένα closure που συλλαμβάνει μεταβλητές από το περιβάλλον πλαίσιο. Στο Android, το callback συνήθως υλοποιείται μέσω ανώνυμων κλάσεων ή εκφράσεων λάμδα Kotlin, που μεταγλωττίζονται σε FunctionalInterface.
Κατά την κλήση μιας ασύγχρονης συνάρτησης, το closure αποθηκεύεται στο heap μαζί με τις συλληφθείσες μεταβλητές. Όταν η λειτουργία ολοκληρωθεί, το σύστημα GCD ή OperationQueue τοποθετεί το callback στην κατάλληλη ουρά (main queue ή background queue). Μετά την εκτέλεση, το callback διαγράφεται από τη μνήμη εάν δεν υπάρχουν ισχυρές αναφορές. Capture list ([weak self]) αποτρέπει τη διατήρηση του αντικειμένου μετά την αποδέσμευσή του. Χωρίς capture list, δημιουργείται retain cycle όπου το αντικείμενο και το callback αναφέρονται αμοιβαία το ένα στο άλλο.
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()
}
// Χρήση με [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)
}
}
Στο Android, το callback μεταβιβάζεται μέσω διεπαφής ή λάμδα. Κατά την εκτέλεση μιας ασύγχρονης λειτουργίας μέσω ExecutorService ή coroutine, το callback αποθηκεύεται στη μνήμη μέχρι την ολοκλήρωση της εργασίας στο παρασκήνιο. Τα λάμδα Kotlin μεταγλωττίζονται σε ανώνυμες κλάσεις που συλλαμβάνουν εξωτερικές μεταβλητές. Η απουσία αδύναμων αναφορών στην JVM απαιτεί χειροκίνητη διαχείριση: μηδενισμός του callback στο onDestroy() ή ακύρωση coroutine μέσω Job.cancel(). Το ViewModel και το LiveData λύνουν αυτό το πρόβλημα σε επίπεδο αρχιτεκτονικού στοιχείου.
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) }
}
}
}
}
// Χρήση με λάμδα
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
Η σύνταξη του callback καθορίζεται από τις γλωσσικές δυνατότητες εργασίας με συναρτήσεις ως αντικείμενα πρώτης τάξης. Στη Swift, τα closures έχουν συνοπτική σύνταξη με αυτόματα ονόματα ορισμάτων ($0, $1). Στην Kotlin, τα λάμδα υποστηρίζουν επίσης το it για ένα μόνο όρισμα. Οι διαφορές εκδηλώνονται στον χειρισμό σύλληψης μεταβλητών (capture list στη Swift έναντι μεταβλητών αναφορών στην Kotlin) και στην τυποποίηση (Result<Success, Failure> έναντι Result<T>).
Το closure στη Swift είναι ένα αυτοδύναμο μπλοκ κώδικα που μπορεί να μεταβιβαστεί και να χρησιμοποιηθεί σε μια άλλη συνάρτηση. Τα closures μπορεί να είναι καθολικά (ονομασμένα), ένθετα και expression-level. @escaping σηματοδοτεί ένα closure που θα εκτελεστεί μετά την επιστροφή από τη συνάρτηση — αυτή είναι υποχρεωτική απαίτηση για ασύγχρονα callback. Χωρίς @escaping, το closure μπορεί να εκτελεστεί μόνο εντός του σώματος της συνάρτησης. Το trailing closure syntax επιτρέπει τη μεταβίβαση του closure μετά από τις στρογγυλές παρενθέσεις: 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 υποστηρίζει συναρτήσεις ανώτερης τάξης που δέχονται άλλες συναρτήσεις ως παραμέτρους. Callback στην Kotlin μεταβιβάζεται μέσω παραμέτρου τύπου (T) -> Unit ή (T) -> R για τιμή επιστροφής. Οι συναρτήσεις suspend των coroutine Kotlin αντικαθιστούν το callback με σειριακό κώδικα, αλλά το callback παραμένει σε συμβατά με Java API και στο Android SDK (View.setOnClickListener, TextWatcher). Τα λάμδα Kotlin συλλαμβάνουν αυτόματα μεταβλητές val, οι μεταβλητές var απαιτούν μεταβλητά περιτυλίγματα.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Παράδειγμα με λάμδα
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — κατάσταση όπου δύο αντικείμενα διατηρούν ισχυρές αναφορές το ένα στο άλλο, εμποδίζοντας την απελευθέρωσή τους από τον συλλέκτη απορριμμάτων. Στη Swift, retain cycle προκύπτει όταν το viewController συλλαμβάνει ένα closure και το closure συλλαμβάνει το self. Στην Kotlin/Java, διαρροή συμβαίνει όταν το Activity μεταβιβάζει μια εσωτερική κλάση ή λάμδα σε μια μακροχρόνια λειτουργία παρασκηνίου. Σύμφωνα με το WWDC Session 10216 (2024), η λανθασμένη διαχείριση closures είναι η τρίτη πιο συχνή αιτία διαρροών μνήμης σε εφαρμογές iOS.
Η Swift χρησιμοποιεί Automatic Reference Counting (ARC), η οποία απελευθερώνει το αντικείμενο όταν ο μετρητής αναφορών μηδενιστεί. Capture list [weak self] ή [unowned self] στο closure αποτρέπει το retain cycle. Το weak self δημιουργεί μια προαιρετική αναφορά που γίνεται nil κατά την αποδέσμευση του αντικειμένου. Το unowned self υποθέτει ότι το αντικείμενο ζει περισσότερο από το closure — η παραβίαση αυτής της υπόθεσης προκαλεί crash. Η χρήση του weak self ως ασφαλούς προεπιλεγμένης επιλογής συνιστάται.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Διορθώθηκε: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
Στο Android, διαρροή callback συμβαίνει όταν το Activity ή Fragment μεταβιβάζει έναν ακροατή σε ένα στοιχείο singleton (π.χ. EventBus ή Service). WeakReference επιτρέπει στον συλλέκτη απορριμμάτων να απελευθερώσει το Activity, ακόμη κι αν υπάρχει αδύναμη αναφορά σε αυτό. Τα στοιχεία Lifecycle-aware (LiveData, Flow) λύνουν το πρόβλημα αυτόματα. Τα λάμδα Kotlin που συλλαμβάνουν το πλαίσιο Activity μπορούν επίσης να προκαλέσουν διαρροή: το λάμδα διατηρεί σιωπηρά μια αναφορά στο 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()
}
}
}
// Χρήση σε Fragment
manager.addListener { result ->
// Το WeakReference δεν κρατά το Fragment
updateUI(result)
}
Callback Hell (γνωστό και ως Πυραμίδα της Καταστροφής) — κατάσταση όπου πολλαπλά ένθετα callback δημιουργούν μια βαθιά ένθετη δομή κώδικα, δύσκολη στην ανάγνωση και τον εντοπισμό σφαλμάτων. Κάθε επόμενο βήμα απαιτεί αναμονή για την ολοκλήρωση του προηγούμενου, οδηγώντας σε 5-10 επίπεδα ένθεσης. Αυτό το πρόβλημα είναι χαρακτηριστικό για σειριακές ασύγχρονες λειτουργίες: φόρτωση δεδομένων → ανάλυση → αποθήκευση στη βάση δεδομένων → ενημέρωση UI.
Η Swift 5.5 εισήγαγε ασύγχρονες συναρτήσεις (async/await) που επιτρέπουν τη σύνταξη ασύγχρονου κώδικα σειριακά. AsyncSequence και AsyncStream αντικαθιστούν τις επαναλήψεις που βασίζονται σε callback. Το πλαίσιο Combine παρέχει τελεστές flatMap, merge, combineLatest για σύνθεση ασύγχρονων ροών χωρίς ένθεση. Ωστόσο, το callback παραμένει απαραίτητο για εργασία με API Objective-C και βιβλιοθήκες τρίτων χωρίς υποστήριξη async.
// Ένθετα callback — 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 — λύση
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)
}
Τα coroutine Kotlin αντικαθιστούν το callback με συναρτήσεις suspend με σειριακή εκτέλεση. Flow παρέχει ψυχρές ροές με τελεστές map, flatMapConcat, combine. Το CoroutineScope επιτρέπει την ακύρωση όλων των εκκινημένων coroutine κατά την καταστροφή του στοιχείου. Τα Room, Retrofit και άλλες βιβλιοθήκες Jetpack έχουν ενσωματωμένη υποστήριξη για συναρτήσεις suspend, εξαλείφοντας την ανάγκη για callback σε τυπικές λειτουργίες.
// Σειριακά callback — 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 — λύση
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 και Delegate — δύο προσεγγίσεις για ασύγχρονη ειδοποίηση και η επιλογή μεταξύ τους εξαρτάται από αρχιτεκτονικές απαιτήσεις. Το Callback είναι κατάλληλο για εφάπαξ λειτουργίες με ένα αποτέλεσμα. Το Delegate προορίζεται για πολλαπλά συμβάντα με διαφορετικές υπογραφές μεθόδων. Η Apple συνιστά το delegate για σύνθετα πρωτόκολλα με πολλαπλές μεθόδους, το callback — για απλά closures με ένα αποτέλεσμα. Στο Android, το callback αντικαθιστά το delegate στις περισσότερες περιπτώσεις λόγω υποστήριξης λάμδα.
Το Callback είναι βέλτιστο για λειτουργίες με ένα μόνο αποτέλεσμα: αίτημα δικτύου, ανάγνωση αρχείου, κινούμενο σχέδιο με μπλοκ ολοκλήρωσης. Πλεονεκτήματα: συνοπτική σύνταξη, χωρίς ξεχωριστό πρωτόκολλο, άμεση σύλληψη πλαισίου. Μειονεκτήματα: πολυπλοκότητα σε πολλαπλά αποτελέσματα (πρόοδος, παύση, ακύρωση), αδυναμία πολλαπλής αποστολής (εάν το callback μπορεί να κληθεί περισσότερες από μία φορές — χρησιμοποιήστε publisher).
Το Delegate είναι κατάλληλο για πρωτόκολλα με πολλαπλές υποχρεωτικές και προαιρετικές μεθόδους: UITableViewDelegate, CLLocationManagerDelegate, συνδέσεις Bluetooth. Πλεονεκτήματα: σαφής τυποποίηση κάθε μεθόδου, τεκμηρίωση μέσω πρωτοκόλλου, υποστήριξη προαιρετικών μεθόδων μέσω @objc optional. Μειονεκτήματα: κώδικας boilerplate, υποχρεωτική αδύναμη αναφορά στο delegate (weak var delegate), πολυπλοκότητα στη σύλληψη πλαισίου.
Συχνές Ερωτήσεις
Callback είναι μια ειδική περίπτωση συνάρτησης ανώτερης τάξης. Μια συνάρτηση ανώτερης τάξης δέχεται μια άλλη συνάρτηση ως όρισμα ή την επιστρέφει. Callback είναι μια συνάρτηση που μεταβιβάζεται ειδικά για ασύγχρονη εκτέλεση μετά την ολοκλήρωση της λειτουργίας. Όλα τα callback υλοποιούνται μέσω συναρτήσεων ανώτερης τάξης, αλλά δεν είναι κάθε συνάρτηση ανώτερης τάξης callback.
Σύμφωνα με τη σύμβαση, το callback πρέπει να κληθεί ακριβώς μία φορά — είτε success είτε failure. Πολλαπλή κλήση του ίδιου callback θεωρείται σφάλμα σχεδιασμού. Για πολλαπλά συμβάντα (πρόοδος, ροή δεδομένων) χρησιμοποιήστε Observable, Publisher ή Flow — υποστηρίζουν πολλαπλή εκπομπή τιμών. Ορισμένα API παραβιάζουν αυτόν τον κανόνα, οδηγώντας σε δυσνόητα σφάλματα.
Trailing closure — συντακτική ζάχαρη στη Swift που επιτρέπει τη μεταβίβαση του closure μετά από τις στρογγυλές παρενθέσεις κλήσης συνάρτησης. Εάν η συνάρτηση δέχεται ένα closure ως τελευταίο όρισμα, μπορεί να τοποθετηθεί εκτός παρενθέσεων: fetchData { result in ... }. Για πολλαπλά closures, το trailing closure εφαρμόζεται μόνο στο τελευταίο, τα υπόλοιπα ονομάζονται εντός παρενθέσεων. Αυτό βελτιώνει την αναγνωσιμότητα των API που βασίζονται σε callback.
Χρησιμοποιήστε WeakReference για μακρόβιους ακροατές, ακυρώστε τα coroutine μέσω Job.cancel() στο onDestroy(), εφαρμόστε lifecycleScope για αυτόματη ακύρωση. Το ViewModel + LiveData/Flow λύνουν το πρόβλημα σε αρχιτεκτονικό επίπεδο. Αποφύγετε τη μεταβίβαση πλαισίου Activity σε στατικά callback — χρησιμοποιήστε πλαίσιο Application. Τα λάμδα Kotlin συλλαμβάνουν σιωπηρά το this, ελέγξτε μέσω memory profiler.
Async/await αντικαθιστά το callback για σειριακό ασύγχρονο κώδικα, αλλά όχι για αρχιτεκτονική event-driven. Το callback παραμένει σε API συστήματος (View.OnClickListener, εκπρόσωποι URLSession), κλήσεις επιστροφής με πρόοδο και βιβλιοθήκες τρίτων. Η πλήρης αντικατάσταση είναι αδύνατη λόγω αντίστροφης συμβατότητας. Η σύγχρονη στρατηγική είναι η χρήση async/await με περιτυλίγματα callback (continuation στη Swift, suspendCancellableCoroutine στην Kotlin).
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης