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 — 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.
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.
| Kryterium | SharedFlow | StateFlow |
|---|---|---|
| Semantyka | Zdarzenia jednorazowe (nawigacja, toast, alert) | Stan UI (lista, ładowanie, błąd) |
| Wartość początkowa | Niewymagana | Wymagana |
| Powtórzenie przy subskrypcji | Tylko jeśli replay > 0 | Zawsze ostatnia wartość |
| Konflacja | Nie — zdarzenia nie są tracone (jeśli bufor nie jest przepełniony) | Tak — przechowuje tylko ostatnie |
| Buforowanie | Konfigurowalne przez replay + extraBufferCapacity | Tylko 1 (replay=1 stały) |
| Zastosowanie | navigationEvent, showSnackbar, openDialog | items, 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 — 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.
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.
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.
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.
Konstruktor MutableSharedFlow przyjmuje trzy parametry określające zachowanie bufora. Nieprawidłowa konfiguracja może prowadzić do utraty zdarzeń lub blokowania emit().
| Parametr | Typ | Domyślnie | Opis |
|---|---|---|---|
| replay | Int | 0 | Liczba ostatnich zdarzeń odtwarzanych nowemu subskrybentowi. 0 = nie odtwarzaj, 1 = jak StateFlow |
| extraBufferCapacity | Int | 0 | Dodatkowy bufor ponad replay. Zdarzenia są przechowywane w buforze cyklicznym. 64 — zalecany limit dla większości scenariuszy |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategia przy zapełnieniu bufora: SUSPEND, DROP_OLDEST, DROP_LATEST |
// 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.
Komendy nawigacyjne — klasyczny use case dla SharedFlow. Fragment subskrybuje się na zdarzenia i wykonuje nawigację. Przy obrocie ekranu komenda się nie powtarza.
// 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()
}
}
}
}
Złożony scenariusz: SharedFlow dla powiadomień o zdarzeniach tła z łączeniem z StateFlow dla 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("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
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.
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.
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.
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ń.
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
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.
Przeczytaj również