SharedFlow: τι είναι, SharedFlow vs StateFlow σε Android

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-20 Χρόνος ανάγνωσης: 9 λεπ

SharedFlow — μια θερμή αντιδραστική ροή από τη βιβλιοθήκη Kotlin Coroutines, βελτιστοποιημένη για εφάπαξ γεγονότα (one-shot events) που δεν πρέπει να επαναλαμβάνονται κατά την περιστροφή της οθόνης ή την αναδημιουργία του συνδρομητή. Δείχνουμε πώς διαφέρει το SharedFlow από το StateFlow: σε αντίθεση με το StateFlow, το SharedFlow δεν αποθηκεύει την τελευταία τιμή για νέους συνδρομητές και υποστηρίζει διαμόρφωση replay, extraBufferCapacity και onBufferOverflow. Σύμφωνα με την Google (Android Developers, 2025), το SharedFlow είναι η προτεινόμενη λύση για εντολές πλοήγησης, μηνύματα Snackbar και άλλα γεγονότα που πρέπει να υποβληθούν σε επεξεργασία ακριβώς μία φορά.

Κύρια Σημεία

  • SharedFlow — hot flow για εφάπαξ γεγονότα: νέοι συνδρομητές δεν λαμβάνουν προηγούμενες τιμές χωρίς ρύθμιση replay.
  • MutableSharedFlow — τροποποιήσιμη έκδοση με μεθόδους emit() και tryEmit() για αποστολή γεγονότων.
  • SharedFlow vs StateFlow: Το SharedFlow δεν συγχωνεύει τιμές (μπορεί να αποθηκεύσει πολλές), δεν απαιτεί αρχική τιμή, είναι κατάλληλο για εφάπαξ γεγονότα.
  • replay — αριθμός τελευταίων γεγονότων που αναπαράγονται σε νέους συνδρομητές (προεπιλογή 0).
  • extraBufferCapacity — επιπλέον προσωρινή μνήμη για γεγονότα πέραν του replay, αποτρέπει το μπλοκάρισμα του emit().

Τι είναι το SharedFlow στο Kotlin;

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 vs StateFlow: πότε να χρησιμοποιήσετε τι

Η επιλογή μεταξύ SharedFlow και StateFlow εξαρτάται από τη σημασιολογία των μεταδιδόμενων δεδομένων: κατάσταση (StateFlow) ή γεγονός (SharedFlow). Παρακάτω υπάρχουν σαφή κριτήρια με παραδείγματα.

ΚριτήριοSharedFlowStateFlow
ΣημασιολογίαΕφάπαξ γεγονότα (πλοήγηση, toast, ειδοποίηση)Κατάσταση UI (λίστα, φόρτωση, σφάλμα)
Αρχική τιμήΔεν απαιτείταιΥποχρεωτική
Επανάληψη κατά εγγραφήΜόνο αν replay > 0Πάντα τελευταία τιμή
ΣυγχώνευσηΌχι — τα γεγονότα δεν χάνονται (αν η προσωρινή μνήμη δεν είναι γεμάτη)Ναι — αποθηκεύει μόνο το τελευταίο
Προσωρινή αποθήκευσηΔιαμορφώσιμη μέσω replay + extraBufferCapacityΜόνο 1 (replay=1 σταθερό)
ΧρήσηnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

Ο απλούστερος κανόνας: εάν τα δεδομένα πρέπει να εμφανίζονται κατά την περιστροφή της οθόνης — είναι κατάσταση (StateFlow). Εάν κατά την περιστροφή της οθόνης το γεγονός δεν πρέπει να επαναληφθεί — είναι εφάπαξ γεγονός (SharedFlow). Για παράδειγμα, „toast με μήνυμα σφάλματος“ — SharedFlow: κατά την περιστροφή το toast δεν πρέπει να εμφανιστεί ξανά. „Λίστα προϊόντων“ — StateFlow: κατά την περιστροφή η λίστα πρέπει να παραμείνει στην οθόνη.

Στην IT Sectr χρησιμοποιούμε το SharedFlow για: εντολές πλοήγησης (μετάβαση σε οθόνη, άνοιγμα deep link), γεγονότα UI (Snackbar, AlertDialog), ειδοποιήσεις συστήματος (ενημέρωση δεδομένων στο παρασκήνιο, αποτέλεσμα πληρωμής), αναλυτικά γεγονότα (καταγραφή, παρακολούθηση).

MutableSharedFlow: emit, tryEmit και προσωρινή αποθήκευση

MutableSharedFlow — τροποποιήσιμη έκδοση του SharedFlow με μεθόδους emit() (suspend) και tryEmit() (non-suspend) για αποστολή γεγονότων. Η emit() αναστέλλεται εάν η προσωρινή μνήμη είναι γεμάτη και onBufferOverflow = SUSPEND. Η tryEmit() επιστρέφει Boolean — εάν το γεγονός προστέθηκε επιτυχώς στην προσωρινή μνήμη.

kotlin
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 γεγονότα ανά δευτερόλεπτο χωρίς απώλειες.

SharedFlow για εφάπαξ γεγονότα: το μοτίβο Event

Το μοτίβο Event (ή UiEvent) — η προτεινόμενη από την Google μέθοδος μεταφοράς εφάπαξ γεγονότων από το ViewModel στο View. Σε αντίθεση με την κατάσταση (StateFlow), το γεγονός πρέπει να υποβληθεί σε επεξεργασία ακριβώς μία φορά και κατά την περιστροφή της οθόνης δεν πρέπει να επαναληφθεί. Το SharedFlow με replay = 0 είναι ιδανικό για αυτήν την εργασία.

kotlin
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 το έχει ήδη απελευθερώσει. Αυτό εγγυάται ότι η πλοήγηση στην οθόνη παρακολούθησης παραγγελίας θα συμβεί μόνο μία φορά, όχι σε κάθε περιστροφή.

Παράμετροι SharedFlow: replay, extraBufferCapacity, onBufferOverflow

Ο κατασκευαστής MutableSharedFlow δέχεται τρεις παραμέτρους που καθορίζουν τη συμπεριφορά της προσωρινής μνήμης. Η λανθασμένη διαμόρφωση μπορεί να οδηγήσει σε απώλεια γεγονότων ή μπλοκάρισμα του emit().

ΠαράμετροςΤύποςΠροεπιλογήΠεριγραφή
replayInt0Αριθμός τελευταίων γεγονότων που αναπαράγονται σε νέο συνδρομητή. 0 = μην αναπαράγεις, 1 = σαν StateFlow
extraBufferCapacityInt0Επιπλέον προσωρινή μνήμη πέραν του replay. Τα γεγονότα αποθηκεύονται σε κυκλική προσωρινή μνήμη. 64 — προτεινόμενο όριο για τα περισσότερα σενάρια
onBufferOverflowBufferOverflowSUSPENDΣτρατηγική όταν γεμίσει η προσωρινή μνήμη: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Διαμορφώσεις για διαφορετικά σενάρια:

// 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 σε Kotlin

Παράδειγμα 1: SharedFlow για πλοήγηση με Jetpack Navigation

Εντολές πλοήγησης — κλασική περίπτωση χρήσης για SharedFlow. Το Fragment εγγράφεται στα γεγονότα και εκτελεί πλοήγηση. Κατά την περιστροφή της οθόνης η εντολή δεν επαναλαμβάνεται.

kotlin
// 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()
            }
        }
    }
}

Παράδειγμα 2: SharedFlow με Room και τελεστές Flow

Σύνθετο σενάριο: SharedFlow για ειδοποιήσεις σχετικά με γεγονότα παρασκηνίου με συνδυασμό με StateFlow για UI.

kotlin
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.

Συχνές Ερωτήσεις

Μπορεί το SharedFlow να χάσει ένα γεγονός;

Ναι, εάν η προσωρινή μνήμη είναι γεμάτη και onBufferOverflow = DROP_OLDEST ή DROP_LATEST. Το SharedFlow δεν εγγυάται την παράδοση κάθε γεγονότος — δεν είναι ουρά μηνυμάτων (όπως το Channel). Εάν χρειάζεται εγγυημένη παράδοση όλων των γεγονότων, χρησιμοποιήστε Channel με μη υπερχειλίσιμη προσωρινή μνήμη (UNLIMITED) ή BroadcastChannel (deprecated). Για γεγονότα UI, η απώλεια παρωχημένων γεγονότων (π.χ. παλιά πλοήγηση) — είναι αναμενόμενη συμπεριφορά, όχι σφάλμα.

Σε τι διαφέρει το SharedFlow από το Channel;

Channel — μια ουρά FIFO όπου κάθε γεγονός παραδίδεται σε ακριβώς έναν συνδρομητή (σημείο-προς-σημείο). SharedFlow — εκπομπή: κάθε γεγονός παραδίδεται σε ΟΛΟΥΣ τους ενεργούς συνδρομητές. Το SharedFlow είναι πιο κοντά στο BroadcastChannel (το οποίο είναι deprecated) και είναι κατάλληλο για σενάρια „ένας-προς-πολλούς“. Channel — για „ένας-προς-έναν“ (δεξαμενές νημάτων, pipeline). Σύμφωνα με τη σύσταση της JetBrains, το SharedFlow είναι ο αντικαταστάτης του BroadcastChannel για όλα τα νέα έργα.

Πώς γίνεται το SharedFlow thread-safe;

Το SharedFlow είναι ήδη thread-safe — τα emit() και collect() είναι σωστά συγχρονισμένα. Πολλαπλά νήματα μπορούν να καλούν το emit() χωρίς μπλοκαρίσματα και όλοι οι ενεργοί συνδρομητές θα λάβουν τα γεγονότα στη σωστή σειρά. Η tryEmit() είναι μη αποκλειστική — επιστρέφει false εάν η προσωρινή μνήμη είναι γεμάτη. Για συστήματα υψηλού φόρτου, χρησιμοποιήστε tryEmit() με DROP_OLDEST — αυτό αποτρέπει το μπλοκάρισμα των νημάτων.

Γιατί το SharedFlow δεν χρησιμοποιείται για κατάσταση;

Το SharedFlow χωρίς replay=1 δεν αποθηκεύει την τελευταία τιμή — κατά την περιστροφή της οθόνης ο νέος συνδρομητής δεν λαμβάνει την τρέχουσα κατάσταση, το UI παραμένει κενό. Με replay=1 το SharedFlow συμπεριφέρεται σαν StateFlow, αλλά χάνει τη βελτιστοποίηση σύγκρισης μέσω equals(), προκαλώντας περιττές ειδοποιήσεις κατά την εκ νέου αποστολή της ίδιας τιμής. StateFlow — η σωστή επιλογή για κατάσταση; SharedFlow — για γεγονότα.

Πώς δοκιμάζουμε το SharedFlow;

Για τη δοκιμή του SharedFlow χρησιμοποιήστε το Turbine — τη βιβλιοθήκη Kotlin για δοκιμή Flow. Το Turbine σας επιτρέπει να ελέγχετε κάθε εκπομπή ξεχωριστά με χρονικά όρια και επαλήθευση ολοκλήρωσης. Παράδειγμα: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Μπορείτε επίσης να χρησιμοποιήσετε .toList() στο runTest με καθορισμό του αριθμού των αναμενόμενων γεγονότων.

Σύνοψη

  • SharedFlow — θερμή αντιδραστική ροή για εφάπαξ γεγονότα, μη δεσμευμένη στην τελευταία κατάσταση.
  • SharedFlow vs StateFlow: SharedFlow — γεγονότα (πλοήγηση, toast, ειδοποιήσεις), StateFlow — κατάσταση (λίστες, φόρτωση, σφάλματα).
  • MutableSharedFlow με replay=0, extraBufferCapacity=5, DROP_OLDEST — τυπική διαμόρφωση για γεγονότα UI.
  • emit() — suspend συνάρτηση για αποκλειστική αποστολή; tryEmit() — non-suspend με αποτέλεσμα Boolean.
  • Το μοτίβο UiEvent με sealed class — προτεινόμενος τρόπος μεταφοράς εφάπαξ γεγονότων από ViewModel σε View.
  • Το SharedFlow εγγυάται παράδοση σε κάθε συνδρομητή, αλλά δεν εγγυάται παράδοση κάθε γεγονότος κατά υπερχείλιση της προσωρινής μνήμης.
  • Ο συνδυασμός SharedFlow + StateFlow σε ένα ViewModel — βέλτιστο μοτίβο για αρχιτεκτονική που διαχωρίζει κατάσταση και γεγονότα.

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης