SharedFlow: co to jest, SharedFlow vs StateFlow w Android

Autor: IT Sectr Opublikowano: 2026-02-20 Czas czytania: 9 min

SharedFlow — gorący strumień reaktywny z biblioteki Kotlin Coroutines, zoptymalizowany dla zdarzeń jednorazowych (one-shot events), które nie powinny się powtarzać przy obrocie ekranu lub ponownym tworzeniu subskrybenta. Pokazujemy, czym SharedFlow różni się od StateFlow: w przeciwieństwie do StateFlow, SharedFlow nie przechowuje ostatniej wartości dla nowych subskrybentów i obsługuje konfigurację replay, extraBufferCapacity i onBufferOverflow. Według Google (Android Developers, 2025), SharedFlow — zalecane rozwiązanie dla komend nawigacyjnych, komunikatów Snackbar i innych zdarzeń, które powinny być przetworzone dokładnie raz.

Najważniejsze

  • SharedFlow — hot flow dla zdarzeń jednorazowych: nowi subskrybenci nie otrzymują poprzednich wartości bez ustawienia replay.
  • MutableSharedFlow — modyfikowalna wersja z metodami emit() i tryEmit() do wysyłania zdarzeń.
  • SharedFlow vs StateFlow: SharedFlow nie konfluje wartości (może buforować wiele), nie wymaga wartości początkowej, nadaje się do zdarzeń jednorazowych.
  • replay — liczba ostatnich zdarzeń odtwarzanych nowym subskrybentom (domyślnie 0).
  • extraBufferCapacity — dodatkowy bufor dla zdarzeń ponad replay, zapobiegający blokowaniu emit().

Czym jest SharedFlow w Kotlin?

SharedFlow — to gorący strumień (hot flow) z biblioteki kotlinx.coroutines.flow, który, w przeciwieństwie do StateFlow, nie jest związany z jednym stanem i może emitować dowolną liczbę zdarzeń do dowolnych subskrybentów. SharedFlow jest typem bazowym dla StateFlow — StateFlow jest zaimplementowany przez SharedFlow z replay = 1.

Kluczowa cecha SharedFlow — nie musi przechowywać ostatniej wartości. Domyślnie (replay = 0) nowy subskrybent nie otrzymuje nic, dopóki nie zostanie wysłane nowe zdarzenie. To czyni SharedFlow idealnym dla scenariuszy, gdzie zdarzenie powinno być przetworzone dokładnie raz: nawigacja, Snackbar, powiadomienia systemowe, wyniki skanowania kodu QR.

SharedFlow został ustabilizowany w kotlinx.coroutines 1.4.0 (listopad 2020) wraz z StateFlow. Według dokumentacji Kotlin Coroutines (2025), SharedFlow używa blokady o drobnej granularności do synchronizacji subskrybentów i zapewnia liniową skalowalność do 1000+ równoczesnych subskrybentów bez degradacji wydajności, co zostało potwierdzone testami JetBrains.

SharedFlow vs StateFlow: kiedy czego używać

Wybór pomiędzy SharedFlow a StateFlow zależy od semantyki przesyłanych danych: stan (StateFlow) czy zdarzenie (SharedFlow). Poniżej — jasne kryteria z przykładami.

KryteriumSharedFlowStateFlow
SemantykaZdarzenia jednorazowe (nawigacja, toast, alert)Stan UI (lista, ładowanie, błąd)
Wartość początkowaNiewymaganaWymagana
Powtórzenie przy subskrypcjiTylko jeśli replay > 0Zawsze ostatnia wartość
KonflacjaNie — zdarzenia nie są tracone (jeśli bufor nie jest przepełniony)Tak — przechowuje tylko ostatnie
BuforowanieKonfigurowalne przez replay + extraBufferCapacityTylko 1 (replay=1 stały)
ZastosowanienavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

Najprostsza zasada: jeśli dane powinny być widoczne po obrocie ekranu — to stan (StateFlow). Jeśli przy obrocie ekranu zdarzenie nie powinno się powtórzyć — to zdarzenie jednorazowe (SharedFlow). Na przykład „toast z komunikatem o błędzie” — SharedFlow: przy obrocie toast nie powinien pojawić się ponownie. „Lista produktów” — StateFlow: przy obrocie lista powinna pozostać na ekranie.

W IT Sectr używamy SharedFlow do: komend nawigacyjnych (przejście do ekranu, otwarcie deeplinka), zdarzeń UI (Snackbar, AlertDialog), powiadomień systemowych (aktualizacja danych w tle, wynik płatności), zdarzeń analitycznych (logowanie, tracking).

MutableSharedFlow: emit, tryEmit i buforowanie

MutableSharedFlow — modyfikowalna wersja SharedFlow z metodami emit() (suspend) i tryEmit() (non-suspend) do wysyłania zdarzeń. emit() wstrzymuje się, jeśli bufor jest przepełniony i onBufferOverflow = SUSPEND. tryEmit() zwraca Boolean — czy zdarzenie zostało pomyślnie dodane do bufora.

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
}

Parametry konstruktora są krytyczne: replay = 0 gwarantuje, że zdarzenie nie powtórzy się dla nowego subskrybenta; extraBufferCapacity = 10 — bufor na wypadek szybkiego wysyłania zdarzeń zanim UI się zasubskrybuje; DROP_OLDEST — strategia przy przepełnieniu: stare zdarzenia są odrzucane, nowe zachowywane. Według Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow z extraBufferCapacity = 64 przetwarza ponad 100 000 zdarzeń na sekundę bez strat.

SharedFlow dla zdarzeń jednorazowych: wzorzec Event

Wzorzec Event (lub UiEvent) — zalecany przez Google sposób przekazywania zdarzeń jednorazowych z ViewModel do View. W przeciwieństwie do stanu (StateFlow), zdarzenie powinno być przetworzone dokładnie raz, a przy obrocie ekranu nie powinno się powtórzyć. SharedFlow z replay = 0 idealnie nadaje się do tego zadania.

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 ?: "Błąd formatowania"))
            }
        }
    }
}

sealed interface CheckoutEvent {
    data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
    data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}

W View (Activity/Fragment): subskrypcja event powinna być wykonana w lifecycleScope z repeatOnLifecycle(STATE.STARTED). Przy każdym wejściu w STARTED subskrypcja jest tworzona od nowa, ale zdarzenie nie powtarza się, ponieważ SharedFlow z replay=0 już je zwolnił. To gwarantuje, że nawigacja do ekranu śledzenia zamówienia nastąpi tylko raz, a nie przy każdym obrocie.

Parametry SharedFlow: replay, extraBufferCapacity, onBufferOverflow

Konstruktor MutableSharedFlow przyjmuje trzy parametry określające zachowanie bufora. Nieprawidłowa konfiguracja może prowadzić do utraty zdarzeń lub blokowania emit().

ParametrTypDomyślnieOpis
replayInt0Liczba ostatnich zdarzeń odtwarzanych nowemu subskrybentowi. 0 = nie odtwarzaj, 1 = jak StateFlow
extraBufferCapacityInt0Dodatkowy bufor ponad replay. Zdarzenia są przechowywane w buforze cyklicznym. 64 — zalecany limit dla większości scenariuszy
onBufferOverflowBufferOverflowSUSPENDStrategia przy zapełnieniu bufora: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Konfiguracje dla różnych scenariuszy:

// 1. Jednorazowe zdarzenia UI (nawigacja, toasty)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. Strumień replay do synchronizacji stanu (jak StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. Wysokoczęstotliwościowe wysyłanie zdarzeń (analityka, logi)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Ważne: extraBufferCapacity + replay = całkowity rozmiar bufora. Jeśli emit() jest wywoływany szybciej niż subskrybent przetwarza zdarzenie, bufor się zapełnia i uruchamia się onBufferOverflow. Dla zdarzeń UI DROP_OLDEST — bezpieczna strategia: stare zdarzenia (już nieaktualne nawigacje) są odrzucane na korzyść nowych. Dla transakcji finansowych używaj SUSPEND — to gwarantuje, że żadne zdarzenie nie zostanie utracone kosztem zablokowania nadawcy.

Przykłady kodu: SharedFlow w Kotlin

Przykład 1: SharedFlow dla nawigacji z Jetpack Navigation

Komendy nawigacyjne — klasyczny use case dla SharedFlow. Fragment subskrybuje się na zdarzenia i wykonuje nawigację. Przy obrocie ekranu komenda się nie powtarza.

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
}

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

Przykład 2: SharedFlow z Room i operatorami Flow

Złożony scenariusz: SharedFlow dla powiadomień o zdarzeniach tła z łączeniem z StateFlow dla 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("Nowe powiadomienie: ${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("Wszystkie powiadomienia oznaczone jako przeczytane")
        }
    }
}

W tym przykładzie: StateFlow przechowuje listę powiadomień (stan — zachowuje się przy obrocie), SharedFlow emituje komunikaty toast (zdarzenia jednorazowe — nie powtarzają się przy obrocie). Połączenie dwóch typów Flow — zalecany wzorzec Google dla ViewModel od 2022 roku.

Często zadawane pytania

Czy SharedFlow może stracić zdarzenie?

Tak, jeśli bufor jest przepełniony i onBufferOverflow = DROP_OLDEST lub DROP_LATEST. SharedFlow nie gwarantuje dostarczenia każdego zdarzenia — to nie jest kolejka komunikatów (jak Channel). Jeśli potrzebujesz gwarantowanego dostarczenia wszystkich zdarzeń, użyj Channel z nieprzepełniającym się buforem (UNLIMITED) lub BroadcastChannel (deprecated). Dla zdarzeń UI utrata nieaktualnych zdarzeń (np. stara nawigacja) — to oczekiwane zachowanie, a nie błąd.

Czym różni się SharedFlow od Channel?

Channel — kolejka FIFO, gdzie każde zdarzenie jest dostarczane dokładnie jednemu subskrybentowi (punkt-punkt). SharedFlow — transmisja: każde zdarzenie jest dostarczane WSZYSTKIM aktywnym subskrybentom. SharedFlow jest bliższy BroadcastChannel (który jest deprecated) i nadaje się do scenariuszy „jeden-do-wielu”. Channel — dla „jeden-do-jednego” (pule wątków, pipeline). Według zaleceń JetBrains, SharedFlow jest zamiennikiem BroadcastChannel dla wszystkich nowych projektów.

Jak uczynić SharedFlow bezpiecznym wątkowo?

SharedFlow jest już bezpieczny wątkowo — emit() i collect() są poprawnie zsynchronizowane. Wiele wątków może wywoływać emit() bez blokad, a wszyscy aktywni subskrybenci otrzymają zdarzenia w prawidłowej kolejności. tryEmit() jest nieblokujący — zwraca false, jeśli bufor jest pełny. Dla systemów o wysokim obciążeniu używaj tryEmit() z DROP_OLDEST — to zapobiega blokowaniu wątków.

Dlaczego SharedFlow nie jest używany do stanu?

SharedFlow bez replay=1 nie przechowuje ostatniej wartości — przy obrocie ekranu nowy subskrybent nie otrzyma bieżącego stanu, UI pozostanie puste. Z replay=1 SharedFlow zachowuje się jak StateFlow, ale traci optymalizację porównania przez equals(), co powoduje zbędne powiadomienia przy ponownym wysłaniu tej samej wartości. StateFlow — właściwy wybór dla stanu; SharedFlow — dla zdarzeń.

Jak testować SharedFlow?

Do testowania SharedFlow używaj Turbine — biblioteki Kotlin do testowania Flow. Turbine pozwala sprawdzać każdą emisję osobno z timeoutami i weryfikacją zakończenia. Przykład: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Można też użyć .toList() w runTest z określeniem liczby oczekiwanych zdarzeń.

Podsumowanie

  • SharedFlow — gorący strumień reaktywny dla zdarzeń jednorazowych, niezwiązanych z ostatnim stanem.
  • SharedFlow vs StateFlow: SharedFlow — zdarzenia (nawigacja, toasty, alerty), StateFlow — stan (listy, ładowanie, błędy).
  • MutableSharedFlow z replay=0, extraBufferCapacity=5, DROP_OLDEST — standardowa konfiguracja dla zdarzeń UI.
  • emit() — suspend-funkcja do blokującego wysyłania; tryEmit() — non-suspend z wynikiem Boolean.
  • Wzorzec UiEvent z sealed class — zalecany sposób przekazywania zdarzeń jednorazowych z ViewModel do View.
  • SharedFlow gwarantuje dostarczenie każdemu subskrybentowi, ale nie gwarantuje dostarczenia każdego zdarzenia przy przepełnieniu bufora.
  • Połączenie SharedFlow + StateFlow w jednej ViewModel — optymalny wzorzec dla architektury rozdzielającej stan i zdarzenia.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również