expect/actual — μηχανισμός του Kotlin Multiplatform που επιτρέπει τη δήλωση πλατφορμο-εξαρτημένων API σε κοινό κώδικα. Η λέξη-κλειδί expect δημιουργεί μια σύμβαση συνάρτησης, κλάσης ή ιδιότητας στο commonMain, ενώ η λέξη-κλειδί actual παρέχει τη συγκεκριμένη υλοποίηση για κάθε πλατφόρμα. Ο μεταγλωττιστής ελέγχει ότι κάθε expect-δήλωση αντιστοιχεί σε actual-υλοποίηση σε όλες τις πλατφόρμες-στόχους. Σύμφωνα με τα δεδομένα του JetBrains, 2025, ο μηχανισμός χρησιμοποιείται στο 80% των KMM-έργων για την υλοποίηση πλατφορμικής επιχειρηματικής λογικής.
Κύρια σημεία
expect/actual — είναι ένας δηλωτικός μηχανισμός του Kotlin Multiplatform για την υλοποίηση πλατφορμο-κατευθυνόμενου προγραμματισμού. Επιτρέπει την περιγραφή ενός API μία φορά στο κοινό module (expect) και την υλοποίησή του ξεχωριστά για κάθε πλατφόρμα (actual). Σε αντίθεση με τις διεπαφές, το expect/actual δεν δημιουργεί εικονικές κλήσεις — ο μεταγλωττιστής συνδέει τις expect- και actual-δηλώσεις στο στάδιο της μεταγλώττισης, αποκλείοντας τα επιπλέον κόστη δυναμικής δρομολόγησης.
Η ιστορία του expect/actual ξεκίνησε με την εμφάνιση του Kotlin Multiplatform το 2017. Αρχικά ο μηχανισμός ονομαζόταν expect/actual declarations και ήταν πειραματικός. Στο Kotlin 1.2 προστέθηκαν expect-σημειώσεις, και στο Kotlin 1.3 το expect/actual έγινε σταθερό για κλάσεις και συναρτήσεις. Σταδιακά ο μηχανισμός επεκτάθηκε: στο Kotlin 1.6 προστέθηκε υποστήριξη για expect/actual σε companion-αντικείμενα, στο Kotlin 1.7 — για enum-κλάσεις, και στο Kotlin 2.0 — για typealias.
Το βασικό χαρακτηριστικό του expect/actual είναι η ασφάλεια σε επίπεδο μεταγλώττισης. Αν ένας προγραμματιστής προσθέσει μια expect-δήλωση στο commonMain αλλά ξεχάσει να παράσχει actual-υλοποίηση για iOS, ο μεταγλωττιστής θα δώσει σφάλμα. Αυτό αποτρέπει σφάλματα χρόνου εκτέλεσης, χαρακτηριστικά για προσεγγίσεις με αντανάκλαση (reflection) ή δυναμική φόρτωση πλατφορμικού κώδικα.
Ο μηχανισμός expect/actual λειτουργεί σε επίπεδο source set — το σύστημα μονάδων του Kotlin Multiplatform. Ο κοινός κώδικας, προσβάσιμος σε όλες τις πλατφόρμες, βρίσκεται στο source set commonMain. Ο πλατφορμο-εξαρτημένος κώδικας — στα iosMain, androidMain, macosMain και ούτω καθεξής. Η λέξη-κλειδί expect στο commonMain δηλώνει ένα API, ενώ η λέξη-κλειδί actual στο πλατφορμικό source set παρέχει την υλοποίηση. Ο μεταγλωττιστής τα συνδέει στο στάδιο της παραγωγής κώδικα, αντικαθιστώντας την κλήση της expect-συνάρτησης με την αντίστοιχη actual-υλοποίηση για την πλατφόρμα-στόχο.
Η ιεραρχία source set σε ένα τυπικό KMM-έργο φαίνεται ως εξής: commonMain περιέχει expect-δηλώσεις, iosMain και androidMain περιέχουν actual-υλοποιήσεις. Κατά τη μεταγλώττιση για iOS χρησιμοποιείται το actual από το iosMain, κατά τη μεταγλώττιση για Android — από το androidMain. Τα source set μπορεί να είναι ενδιάμεσα (π.χ. iosArm64Main για συγκεκριμένη αρχιτεκτονική), επιτρέποντας την εξειδίκευση υλοποιήσεων για διαφορετικές συσκευές.
// commonMain — δήλωση expect
expect fun getPlatformName(): String
// androidMain — actual για Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual για iOS
actual fun getPlatformName(): String = "iOS"
Ο μεταγλωττιστής Kotlin ελέγχει αρκετές προϋποθέσεις κατά την εργασία με expect/actual. Κάθε expect-δήλωση πρέπει να έχει actual-υλοποίηση για κάθε ενεργή πλατφόρμα. Η υπογραφή της actual-δήλωσης πρέπει να ταιριάζει με την expect-υπογραφή (η σημείωση @OptionalExpectation μπορεί να χαλαρώνει αυτή την απαίτηση). Οι τροποποιητές πρόσβασης, ο τύπος επιστροφής και οι παράμετροι πρέπει να είναι πανομοιότυποι. Ο μεταγλωττιστής επίσης ελέγχει την απουσία κυκλικών εξαρτήσεων μεταξύ expect- και actual-δηλώσεων.
Το expect/actual υποστηρίζει διάφορους τύπους δηλώσεων. Οι πιο συχνά χρησιμοποιούμενοι είναι οι expect/actual-συναρτήσεις για πλατφορμικές λειτουργίες, οι expect/actual-κλάσεις για αντικείμενα που απαιτούν εγγενή υλοποίηση, και οι expect/actual-ιδιότητες για σταθερές και ρυθμίσεις. Κάθε τύπος έχει τους δικούς του κανόνες χρήσης και περιορισμούς.
Οι expect/actual-συναρτήσεις — ο απλούστερος και πιο διαδεδομένος τύπος. Χρησιμοποιούνται για κλήση πλατφορμικών API, όπως λήψη ώρας, ανάγνωση αρχείων ή αποστολή HTTP-αιτημάτων. Οι expect/actual-κλάσεις εφαρμόζονται για δημιουργία αντικειμένων που αλληλεπιδρούν άμεσα με εγγενή κώδικα (π.χ. για πρόσβαση σε κάμερα, γεωτοποθεσία ή ασφαλές αποθηκευτικό χώρο). Οι expect/actual-ιδιότητες (val) είναι κατάλληλες για πλατφορμικές σταθερές — όνομα ΛΣ, έκδοση SDK ή διαδρομή συστήματος.
| Τύπος δήλωσης | Λέξεις-κλειδιά | Παράδειγμα χρήσης |
|---|---|---|
| Συνάρτηση | expect fun / actual fun | Λήψη μοναδικού αναγνωριστικού συσκευής |
| Κλάση | expect class / actual class | Πρόσβαση σε SecureStorage (Keychain / EncryptedSharedPreferences) |
| Ιδιότητα | expect val / actual val | Τρέχουσα πλατφόρμα (iOS / Android) |
| Enum-κλάση | expect enum / actual enum | Λίστα διαθέσιμων αδειών εφαρμογής |
| Typealias | expect typealias / actual typealias | Τύπος δικτυακής απόκρισης, ειδικός για πλατφόρμα |
Δεν μπορούν όλες οι κατασκευές της Kotlin να χρησιμοποιηθούν με expect/actual. Η expect-δήλωση δεν μπορεί να περιέχει σώμα — μόνο υπογραφή. Η expect-κλάση δεν μπορεί να έχει constructor με παραμέτρους (πρέπει να έχει κενό primary constructor). Για enum expect/actual όλες οι σταθερές πρέπει να είναι ίδιες σε expect και actual. Οι expect-ιδιότητες πρέπει να είναι val (όχι var), καθώς η αποθήκευση κατάστασης στο κοινό module για πλατφορμικές ιδιότητες δεν έχει νόημα.
Ας εξετάσουμε πρακτικά παραδείγματα expect/actual από απλές συναρτήσεις έως πλήρεις κλάσεις. Η βασική περίπτωση — λήψη του ονόματος πλατφόρμας για χρήση στο UI. Πιο σύνθετα παραδείγματα περιλαμβάνουν πρόσβαση σε εγγενή αποθηκευτικό χώρο και εργασία με πλατφορμικά νήματα.
// commonMain — κλάση expect για ασφαλή αποθήκευση
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual στο Android
actual class PlatformStorage {
private val prefs = AppContext.getSharedPreferences("secure", 0)
actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
actual fun get(key: String): String? = prefs.getString(key, null)
actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}
Σε αυτό το παράδειγμα η expect-κλάση PlatformStorage ορίζει μια σύμβαση απλού αποθηκευτικού χώρου κλειδιού-τιμής. Στο Android η υλοποίηση χρησιμοποιεί SharedPreferences, ενώ στο iOS — Keychain ή NSUserDefaults. Χάρη στο expect/actual, η επιχειρηματική λογική στο commonMain καλεί save/get/remove, χωρίς να γνωρίζει την πλατφορμική υλοποίηση.
// iosMain — actual στο iOS με Keychain
actual class PlatformStorage {
actual fun save(key: String, value: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecValueData to value.encodeToByteArray()
)
SecItemAdd(query, null)
}
actual fun get(key: String): String? {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecReturnData to true
)
val result = mutableMapOf<String, Any>()
return if (SecItemCopyMatching(query, result) == errSecSuccess)
result[kSecValueData]?.toString()
else null
}
actual fun remove(key: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key
)
SecItemDelete(query)
}
}
Κατά τον σχεδιασμό ενός expect/actual API θα πρέπει να ακολουθούνται αρκετές αρχές. Ελαχιστοποιήστε τον αριθμό expect-δηλώσεων — όσο περισσότερος κοινός κώδικας, τόσο πιο εύκολη η συντήρηση. Χρησιμοποιήστε expect/actual μόνο για εκείνα τα API που πραγματικά διαφέρουν στις πλατφόρμες. Για τον υπόλοιπο κώδικα εφαρμόστε διεπαφές με εργοστάσια ή dependency injection, που απλοποιεί τη δοκιμή.
Συνιστάται η ομαδοποίηση expect-δηλώσεων κατά θεματικές ενότητες, αντί να αναμιγνύονται σε ένα αρχείο. Για παράδειγμα, Storage.kt για expect-δηλώσεις αποθηκευτικού χώρου, Platform.kt για expect-συναρτήσεις εργασίας με το ΛΣ και Analytics.kt για expect-κλάσεις αναλυτικής. Αυτό απλοποιεί την πλοήγηση και την κατανόηση της πλατφορμικής επιφάνειας ενός KMM-έργου. Κάθε actual-αρχείο πρέπει να βρίσκεται στο αντίστοιχο source set: androidMain, iosMain, desktopMain και ούτω καθεξής.
Οι προεπιλεγμένες υλοποιήσεις μέσω expect fun με actual fun, όπου το actual χρησιμοποιεί common-κώδικα, είναι ένα συνηθισμένο αντι-μοτίβο. Αν η πλατφορμική υλοποίηση δεν διαφέρει από την προεπιλεγμένη, το expect/actual δεν είναι απαραίτητο. Σε τέτοιες περιπτώσεις χρησιμοποιήστε μια απλή συνάρτηση στο commonMain. Επίσης αποφεύγετε το expect/actual για τετριμμένες μεθόδους λήψης (getters) — χρησιμοποιήστε expect val με σταθερές.
Η σωστή δομή του expect/actual κώδικα είναι κρίσιμη για την αναγνωσιμότητα του έργου. Κάθε expect/actual module πρέπει να έχει ένα ενιαίο σημείο εισόδου. Παράδειγμα οργάνωσης: commonMain/kotlin/com/project/platform περιέχει expect-δηλώσεις, androidMain/kotlin/com/project/platform — actual για Android, iosMain/kotlin/com/project/platform — actual για iOS. Τα ονόματα αρχείων και πακέτων πρέπει να ταιριάζουν για expect και actual, ώστε ο προγραμματιστής να μπορεί να βρει γρήγορα την αντίστοιχη υλοποίηση.
Οι διεπαφές με πλατφορμικό εργοστάσιο — η κύρια εναλλακτική του expect/actual. Αντί για expect-κλάση μπορείτε να δηλώσετε μια διεπαφή στο commonMain, και να δημιουργήσετε συγκεκριμένες κλάσεις στα πλατφορμικά modules. Ένα εργοστάσιο ή ένα container dependency injection παρέχει τη σωστή υλοποίηση κατά τον χρόνο εκτέλεσης. Αυτή η προσέγγιση είναι καλύτερη για δοκιμές, καθώς η διεπαφή μπορεί να υποκατασταθεί (mock).
Dependency Injection (Koin, Kodein) — μια πιο ευέλικτη αλλά λιγότερο αποδοτική προσέγγιση. Το DI-container ρυθμίζεται ξεχωριστά για κάθε πλατφόρμα και παρέχει πλατφορμικές εξαρτήσεις στον κοινό κώδικα. Σε αντίθεση με το expect/actual, η έγχυση γίνεται κατά τον χρόνο εκτέλεσης, δίνοντας τη δυνατότητα αλλαγής υλοποιήσεων για δοκιμές. Από την άλλη πλευρά, σφάλματα διαμόρφωσης DI εντοπίζονται μόνο κατά την εκκίνηση, όχι στο στάδιο της μεταγλώττισης.
| Προσέγγιση | Έλεγχος κατά τη μεταγλώττιση | Ευελιξία δοκιμών | Επιπλέον κόστος χρόνου εκτέλεσης |
|---|---|---|---|
| expect/actual | Πλήρης | Χαμηλή (actual δεν μπορεί να υποκατασταθεί) | Μηδενικό (σύνδεση στη μεταγλώττιση) |
| Διεπαφές + εργοστάσιο | Μερική | Υψηλή (μπορεί να υποκατασταθεί) | Ελάχιστο (εικονική κλήση) |
| Dependency Injection | Όχι (χρόνος εκτέλεσης) | Υψηλή | Μέτριο (DI-proxy) |
Η επιλογή μεταξύ expect/actual και εναλλακτικών εξαρτάται από το πλαίσιο. Για κρίσιμη απόδοση (μηχανές παιχνιδιών, επεξεργασία πραγματικού χρόνου) το expect/actual είναι προτιμότερο λόγω μηδενικών επιπλέον εξόδων. Για επιχειρηματική λογική (αποθετήρια, use-cases) είναι καλύτερο να χρησιμοποιούνται διεπαφές με DI για απλοποίηση δοκιμών. Η συνδυασμένη προσέγγιση — expect/actual για χαμηλού επιπέδου πλατφορμικές λειτουργίες και διεπαφές για το επίπεδο επιχειρηματικής λογικής — εφαρμόζεται στα περισσότερα KMM-έργα παραγωγής.
Συχνές ερωτήσεις
Το expect/actual συνδέει την υλοποίηση στο στάδιο της μεταγλώττισης χωρίς εικονικές κλήσεις, ενώ οι διεπαφές — κατά τον χρόνο εκτέλεσης. Το expect/actual εγγυάται την ύπαρξη υλοποίησης για όλες τις πλατφόρμες, οι διεπαφές απαιτούν ελέγχους χρόνου εκτέλεσης.
Ναι, το expect enum υποστηρίζεται από το Kotlin 1.7 και μετά. Όλες οι σταθερές στο expect και actual enum πρέπει να ταιριάζουν. Διαφορετικές τιμές σταθερών σε διαφορετικές πλατφόρμες αποτελούν σφάλμα μεταγλώττισης.
Ο μεταγλωττιστής θα δώσει σφάλμα για κάθε πλατφόρμα όπου λείπει η actual-υλοποίηση. Το έργο δεν θα μεταγλωττιστεί μέχρι να προστεθούν οι αντίστοιχες actual-υλοποιήσεις για όλες τις expect-δηλώσεις.
Όχι, το expect και το actual πρέπει να βρίσκονται σε διαφορετικά source set. Το expect — στο commonMain ή σε ενδιάμεσο source set, το actual — στο πλατφορμικό source set. Η τοποθέτηση expect και actual στο ίδιο source set αποτελεί σφάλμα μεταγλώττισης.
Για δοκιμή expect/actual χρησιμοποιήστε commonTest με πλατφορμικά test source set. Γράψτε expect-δοκιμές στο commonTest και actual-δοκιμές για κάθε πλατφόρμα. Οι δοκιμές ολοκλήρωσης εκτελούνται ξεχωριστά σε κάθε πλατφόρμα-στόχο.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης