SharedFlow — μια θερμή αντιδραστική ροή από τη βιβλιοθήκη Kotlin Coroutines, βελτιστοποιημένη για εφάπαξ γεγονότα (one-shot events) που δεν πρέπει να επαναλαμβάνονται κατά την περιστροφή της οθόνης ή την αναδημιουργία του συνδρομητή. Δείχνουμε πώς διαφέρει το SharedFlow από το StateFlow: σε αντίθεση με το StateFlow, το SharedFlow δεν αποθηκεύει την τελευταία τιμή για νέους συνδρομητές και υποστηρίζει διαμόρφωση replay, extraBufferCapacity και onBufferOverflow. Σύμφωνα με την Google (Android Developers, 2025), το SharedFlow είναι η προτεινόμενη λύση για εντολές πλοήγησης, μηνύματα Snackbar και άλλα γεγονότα που πρέπει να υποβληθούν σε επεξεργασία ακριβώς μία φορά.
Κύρια Σημεία
SharedFlow — είναι μια θερμή ροή (hot flow) από τη βιβλιοθήκη kotlinx.coroutines.flow που, σε αντίθεση με το StateFlow, δεν είναι δεσμευμένη σε μία κατάσταση και μπορεί να εκπέμψει οποιονδήποτε αριθμό γεγονότων σε οποιουσδήποτε συνδρομητές. Το SharedFlow είναι ο βασικός τύπος για το StateFlow — το StateFlow υλοποιείται μέσω του SharedFlow με replay = 1.
Το βασικό χαρακτηριστικό του SharedFlow — δεν είναι υποχρεωμένο να αποθηκεύει την τελευταία τιμή. Από προεπιλογή (replay = 0) ένας νέος συνδρομητής δεν λαμβάνει τίποτα μέχρι να σταλεί ένα νέο γεγονός. Αυτό καθιστά το SharedFlow ιδανικό για σενάρια όπου το γεγονός πρέπει να υποβληθεί σε επεξεργασία ακριβώς μία φορά: πλοήγηση, Snackbar, ειδοποιήσεις συστήματος, αποτελέσματα σάρωσης κωδικού QR.
Το SharedFlow σταθεροποιήθηκε στο kotlinx.coroutines 1.4.0 (Νοέμβριος 2020) μαζί με το StateFlow. Σύμφωνα με την τεκμηρίωση του Kotlin Coroutines (2025), το SharedFlow χρησιμοποιεί κλείδωμα λεπτής κοκκομετρίας για συγχρονισμό συνδρομητών και παρέχει γραμμική επεκτασιμότητα έως 1000+ ταυτόχρονους συνδρομητές χωρίς υποβάθμιση απόδοσης, όπως επιβεβαιώνεται από δοκιμές της JetBrains.
Η επιλογή μεταξύ SharedFlow και StateFlow εξαρτάται από τη σημασιολογία των μεταδιδόμενων δεδομένων: κατάσταση (StateFlow) ή γεγονός (SharedFlow). Παρακάτω υπάρχουν σαφή κριτήρια με παραδείγματα.
| Κριτήριο | SharedFlow | StateFlow |
|---|---|---|
| Σημασιολογία | Εφάπαξ γεγονότα (πλοήγηση, toast, ειδοποίηση) | Κατάσταση UI (λίστα, φόρτωση, σφάλμα) |
| Αρχική τιμή | Δεν απαιτείται | Υποχρεωτική |
| Επανάληψη κατά εγγραφή | Μόνο αν replay > 0 | Πάντα τελευταία τιμή |
| Συγχώνευση | Όχι — τα γεγονότα δεν χάνονται (αν η προσωρινή μνήμη δεν είναι γεμάτη) | Ναι — αποθηκεύει μόνο το τελευταίο |
| Προσωρινή αποθήκευση | Διαμορφώσιμη μέσω replay + extraBufferCapacity | Μόνο 1 (replay=1 σταθερό) |
| Χρήση | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Ο απλούστερος κανόνας: εάν τα δεδομένα πρέπει να εμφανίζονται κατά την περιστροφή της οθόνης — είναι κατάσταση (StateFlow). Εάν κατά την περιστροφή της οθόνης το γεγονός δεν πρέπει να επαναληφθεί — είναι εφάπαξ γεγονός (SharedFlow). Για παράδειγμα, „toast με μήνυμα σφάλματος“ — SharedFlow: κατά την περιστροφή το toast δεν πρέπει να εμφανιστεί ξανά. „Λίστα προϊόντων“ — StateFlow: κατά την περιστροφή η λίστα πρέπει να παραμείνει στην οθόνη.
Στην IT Sectr χρησιμοποιούμε το SharedFlow για: εντολές πλοήγησης (μετάβαση σε οθόνη, άνοιγμα deep link), γεγονότα UI (Snackbar, AlertDialog), ειδοποιήσεις συστήματος (ενημέρωση δεδομένων στο παρασκήνιο, αποτέλεσμα πληρωμής), αναλυτικά γεγονότα (καταγραφή, παρακολούθηση).
MutableSharedFlow — τροποποιήσιμη έκδοση του SharedFlow με μεθόδους emit() (suspend) και tryEmit() (non-suspend) για αποστολή γεγονότων. Η emit() αναστέλλεται εάν η προσωρινή μνήμη είναι γεμάτη και onBufferOverflow = SUSPEND. Η tryEmit() επιστρέφει Boolean — εάν το γεγονός προστέθηκε επιτυχώς στην προσωρινή μνήμη.
class EventBus {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> get() = _events
suspend fun sendEvent(event: UiEvent) {
_events.emit(event)
}
fun trySendEvent(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
}
sealed interface UiEvent {
data class ShowSnackbar(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
data class ShowDialog(val title: String, val message: String) : UiEvent
}
Οι παράμετροι του κατασκευαστή είναι κρίσιμες: replay = 0 εγγυάται ότι το γεγονός δεν θα επαναληφθεί για νέο συνδρομητή; extraBufferCapacity = 10 — προσωρινή μνήμη για γρήγορη αποστολή γεγονότων πριν εγγραφεί το UI; DROP_OLDEST — στρατηγική υπερχείλισης: παλιά γεγονότα απορρίπτονται, νέα διατηρούνται. Σύμφωνα με το Kotlin Coroutines Performance (JetBrains, 2024), το SharedFlow με extraBufferCapacity = 64 επεξεργάζεται πάνω από 100.000 γεγονότα ανά δευτερόλεπτο χωρίς απώλειες.
Το μοτίβο Event (ή UiEvent) — η προτεινόμενη από την Google μέθοδος μεταφοράς εφάπαξ γεγονότων από το ViewModel στο View. Σε αντίθεση με την κατάσταση (StateFlow), το γεγονός πρέπει να υποβληθεί σε επεξεργασία ακριβώς μία φορά και κατά την περιστροφή της οθόνης δεν πρέπει να επαναληφθεί. Το SharedFlow με replay = 0 είναι ιδανικό για αυτήν την εργασία.
class CheckoutViewModel : ViewModel() {
private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
val uiState: StateFlow<CheckoutState> get() = _uiState
private val _event = MutableSharedFlow<CheckoutEvent>()
val event: SharedFlow<CheckoutEvent> get() = _event
fun placeOrder() {
viewModelScope.launch {
_uiState.value = CheckoutState.Loading
try {
val orderId = orderRepository.createOrder(cart)
_uiState.value = CheckoutState.Success(orderId)
_event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
} catch (e: Exception) {
_uiState.value = CheckoutState.Error(e.message)
_event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "Σφάλμα μορφοποίησης"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Στο View (Activity/Fragment): η εγγραφή στο γεγονός πρέπει να εκτελείται στο lifecycleScope με repeatOnLifecycle(STATE.STARTED). Σε κάθε είσοδο στο STARTED η εγγραφή δημιουργείται ξανά, αλλά το γεγονός δεν επαναλαμβάνεται επειδή το SharedFlow με replay=0 το έχει ήδη απελευθερώσει. Αυτό εγγυάται ότι η πλοήγηση στην οθόνη παρακολούθησης παραγγελίας θα συμβεί μόνο μία φορά, όχι σε κάθε περιστροφή.
Ο κατασκευαστής MutableSharedFlow δέχεται τρεις παραμέτρους που καθορίζουν τη συμπεριφορά της προσωρινής μνήμης. Η λανθασμένη διαμόρφωση μπορεί να οδηγήσει σε απώλεια γεγονότων ή μπλοκάρισμα του emit().
| Παράμετρος | Τύπος | Προεπιλογή | Περιγραφή |
|---|---|---|---|
| replay | Int | 0 | Αριθμός τελευταίων γεγονότων που αναπαράγονται σε νέο συνδρομητή. 0 = μην αναπαράγεις, 1 = σαν StateFlow |
| extraBufferCapacity | Int | 0 | Επιπλέον προσωρινή μνήμη πέραν του replay. Τα γεγονότα αποθηκεύονται σε κυκλική προσωρινή μνήμη. 64 — προτεινόμενο όριο για τα περισσότερα σενάρια |
| onBufferOverflow | BufferOverflow | SUSPEND | Στρατηγική όταν γεμίσει η προσωρινή μνήμη: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Διαμορφώσεις για διαφορετικά σενάρια:
// 1. Εφάπαξ γεγονότα UI (πλοήγηση, toast)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Ροή replay για συγχρονισμό κατάστασης (σαν StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Αποστολή γεγονότων υψηλής συχνότητας (αναλυτικά, αρχεία καταγραφής)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Σημαντικό: extraBufferCapacity + replay = συνολικό μέγεθος προσωρινής μνήμης. Εάν η emit() καλείται ταχύτερα από όσο ο συνδρομητής επεξεργάζεται το γεγονός, η προσωρινή μνήμη γεμίζει και ενεργοποιείται το onBufferOverflow. Για γεγονότα UI, το DROP_OLDEST είναι ασφαλής στρατηγική: παλιά γεγονότα (μη πλέον σχετικές πλοηγήσεις) απορρίπτονται υπέρ των νέων. Για οικονομικές συναλλαγές χρησιμοποιήστε SUSPEND — αυτό εγγυάται ότι κανένα γεγονός δεν θα χαθεί με κόστος το μπλοκάρισμα του αποστολέα.
Εντολές πλοήγησης — κλασική περίπτωση χρήσης για SharedFlow. Το Fragment εγγράφεται στα γεγονότα και εκτελεί πλοήγηση. Κατά την περιστροφή της οθόνης η εντολή δεν επαναλαμβάνεται.
// ViewModel
class AuthViewModel : ViewModel() {
private val _navEvent = MutableSharedFlow<NavEvent>()
val navEvent: SharedFlow<NavEvent> get() = _navEvent
fun onLoginSuccess() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
}
}
fun onLogout() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
}
}
}
sealed interface NavEvent {
data class NavigateTo(val route: String) : NavEvent
data class NavigateBack(val popUpTo: String? = null) : NavEvent
}
// Στο Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
is NavEvent.NavigateBack -> findNavController().popBackStack()
}
}
}
}
Σύνθετο σενάριο: SharedFlow για ειδοποιήσεις σχετικά με γεγονότα παρασκηνίου με συνδυασμό με StateFlow για UI.
class NotificationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> get() = _toastMessage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> get() = _notifications
init {
viewModelScope.launch {
notificationChannel
.consumeAsFlow()
.collect { notification ->
_notifications.value = _notifications.value + notification
_toastMessage.emit("Νέα ειδοποίηση: ${notification.title}")
}
}
}
fun dismissNotification(id: String) {
_notifications.value = _notifications.value.filter { it.id != id }
}
fun markAllRead() {
viewModelScope.launch {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
_toastMessage.emit("Όλες οι ειδοποιήσεις επισημάνθηκαν ως αναγνωσμένες")
}
}
}
Σε αυτό το παράδειγμα: το StateFlow αποθηκεύει τη λίστα ειδοποιήσεων (κατάσταση — διατηρείται κατά την περιστροφή), το SharedFlow εκπέμπει μηνύματα toast (εφάπαξ γεγονότα — δεν επαναλαμβάνονται κατά την περιστροφή). Ο συνδυασμός δύο τύπων Flow — το προτεινόμενο μοτίβο της Google για ViewModel από το 2022.
Συχνές Ερωτήσεις
Ναι, εάν η προσωρινή μνήμη είναι γεμάτη και onBufferOverflow = DROP_OLDEST ή DROP_LATEST. Το SharedFlow δεν εγγυάται την παράδοση κάθε γεγονότος — δεν είναι ουρά μηνυμάτων (όπως το Channel). Εάν χρειάζεται εγγυημένη παράδοση όλων των γεγονότων, χρησιμοποιήστε Channel με μη υπερχειλίσιμη προσωρινή μνήμη (UNLIMITED) ή BroadcastChannel (deprecated). Για γεγονότα UI, η απώλεια παρωχημένων γεγονότων (π.χ. παλιά πλοήγηση) — είναι αναμενόμενη συμπεριφορά, όχι σφάλμα.
Channel — μια ουρά FIFO όπου κάθε γεγονός παραδίδεται σε ακριβώς έναν συνδρομητή (σημείο-προς-σημείο). SharedFlow — εκπομπή: κάθε γεγονός παραδίδεται σε ΟΛΟΥΣ τους ενεργούς συνδρομητές. Το SharedFlow είναι πιο κοντά στο BroadcastChannel (το οποίο είναι deprecated) και είναι κατάλληλο για σενάρια „ένας-προς-πολλούς“. Channel — για „ένας-προς-έναν“ (δεξαμενές νημάτων, pipeline). Σύμφωνα με τη σύσταση της JetBrains, το SharedFlow είναι ο αντικαταστάτης του BroadcastChannel για όλα τα νέα έργα.
Το SharedFlow είναι ήδη thread-safe — τα emit() και collect() είναι σωστά συγχρονισμένα. Πολλαπλά νήματα μπορούν να καλούν το emit() χωρίς μπλοκαρίσματα και όλοι οι ενεργοί συνδρομητές θα λάβουν τα γεγονότα στη σωστή σειρά. Η tryEmit() είναι μη αποκλειστική — επιστρέφει false εάν η προσωρινή μνήμη είναι γεμάτη. Για συστήματα υψηλού φόρτου, χρησιμοποιήστε tryEmit() με DROP_OLDEST — αυτό αποτρέπει το μπλοκάρισμα των νημάτων.
Το SharedFlow χωρίς replay=1 δεν αποθηκεύει την τελευταία τιμή — κατά την περιστροφή της οθόνης ο νέος συνδρομητής δεν λαμβάνει την τρέχουσα κατάσταση, το UI παραμένει κενό. Με replay=1 το SharedFlow συμπεριφέρεται σαν StateFlow, αλλά χάνει τη βελτιστοποίηση σύγκρισης μέσω equals(), προκαλώντας περιττές ειδοποιήσεις κατά την εκ νέου αποστολή της ίδιας τιμής. StateFlow — η σωστή επιλογή για κατάσταση; SharedFlow — για γεγονότα.
Για τη δοκιμή του SharedFlow χρησιμοποιήστε το Turbine — τη βιβλιοθήκη Kotlin για δοκιμή Flow. Το Turbine σας επιτρέπει να ελέγχετε κάθε εκπομπή ξεχωριστά με χρονικά όρια και επαλήθευση ολοκλήρωσης. Παράδειγμα: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Μπορείτε επίσης να χρησιμοποιήσετε .toList() στο runTest με καθορισμό του αριθμού των αναμενόμενων γεγονότων.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης