Kotlin Flow vs StateFlow vs SharedFlow: pytania rekrutacyjne Android w 2026

Pytania o Kotlin Flow vs StateFlow vs SharedFlow, które rekruterzy Androida zadają w 2026 roku, z jasnymi odpowiedziami, tabelą porównawczą i kodem gotowym do produkcji.

Porównanie Kotlin Flow vs StateFlow vs SharedFlow do rozmów rekrutacyjnych Android

Kotlin Flow vs StateFlow vs SharedFlow to jeden z najczęstszych tematów rozmów kwalifikacyjnych dla programistów Androida w 2026 roku, a pomylenie tych trzech typów potrafi szybko przekreślić szanse na rundzie poświęconej korutynom. StateFlow i SharedFlow to strumienie gorące zbudowane na bazie Flow, jednak każdy z nich rozwiązuje inny problem: przechowywanie stanu kontra rozgłaszanie zdarzeń. Poniższe pytania rzeczywiście padają na rozmowach z rekruterami technicznymi Androida, wraz z precyzyjnymi odpowiedziami i kodem gotowym do produkcji.

Odpowiedź w 20 sekund

Flow jest zimny i uruchamia swojego producenta osobno dla każdego kolektora. StateFlow to gorący, sklejający (conflated) strumień, który zawsze przechowuje jedną bieżącą wartość, idealny do stanu UI. SharedFlow to gorący strumień bez wymaganej wartości początkowej, idealny do jednorazowych zdarzeń, takich jak nawigacja czy snackbar.

Kotlin Flow vs StateFlow vs SharedFlow: kluczowe różnice

StateFlow i SharedFlow to specjalizacje SharedFlow, który sam w sobie jest gorącym Flow. Rekruterzy sprawdzają rozróżnienie na strumienie zimne i gorące, to, czy bieżąca wartość jest zachowywana, oraz jak każdy z typów zachowuje się przy zduplikowanych emisjach. Zwięzły model myślowy: Flow to przepis, StateFlow to pojedyncza zmienna wartość, a SharedFlow to szyna zdarzeń (event bus).

| Właściwość | Flow (zimny) | StateFlow | SharedFlow | |---|---|---|---| | Temperatura | Zimny | Gorący | Gorący | | Wartość początkowa | Brak | Wymagana | Opcjonalna (przez replay) | | Przechowuje bieżącą wartość | Nie | Tak, przez .value | Nie | | Emituje duplikaty | Tak | Nie (sklejanie + deduplikacja) | Konfigurowalne | | Dodatkowe kolektory | Za każdym razem nowy strumień | Współdzielony | Współdzielony | | Najlepszy do | Asynchroniczne potoki danych | Stan UI | Jednorazowe zdarzenia |

Oficjalna dokumentacja korutyn Kotlina traktuje tę domyślną zimność jako cechę definiującą Flow, więc mocna odpowiedź powinna zacząć się właśnie od tego.

Dlaczego Kotlin Flow jest domyślnie zimny?

Zimny flow nie robi nic, dopóki nie zostanie wywołane collect(), a swój blok producenta wykonuje ponownie dla każdego kolektora. Dwa kolektory podpięte do tego samego zimnego flow wywołają dwa niezależne zapytania sieciowe. To najczęściej sprawdzana koncepcja, dlatego krótki przykład dobrze ilustruje odpowiedź.

SearchRepository.ktkotlin
fun searchResults(query: String): Flow<List<Result>> = flow {
    // This block runs fresh for every collector.
    // Nothing executes until a collector calls collect().
    val results = api.search(query) // suspending network call
    emit(results)                   // pushed downstream to the collector
}

Ponieważ producent uruchamia się od nowa dla każdego kolektora, zimne flow są właściwym domyślnym wyborem dla potoków danych, które mają działać na żądanie. Stają się problemem dopiero wtedy, gdy jedna wartość musi być współdzielona przez cały ekran, i dokładnie tu wkraczają StateFlow oraz SharedFlow.

Pytania o StateFlow: stan, który zawsze emituje

StateFlow to gorący flow, który zawsze ma wartość i odtwarza tę najnowszą wartość każdemu nowemu kolektorowi. Wymaga wartości początkowej, udostępnia .value do synchronicznego odczytu i skleja emisje: szybkie kolejne aktualizacje mogą pomijać wartości pośrednie, a ustawienie tej samej wartości dwukrotnie nie emituje nic, ponieważ deduplikacja odbywa się przez porównanie równości. Czyni to z niego nowoczesne zastępstwo dla LiveData w ViewModel.

SearchViewModel.ktkotlin
class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
    // MutableStateFlow demands an initial value, so the UI always has something to render.
    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Idle)
    val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()

    fun onQueryChanged(query: String) {
        _uiState.value = SearchUiState.Loading
        viewModelScope.launch {
            val results = repo.search(query)
            // update {} applies the change atomically, safe under concurrent callers.
            _uiState.update { SearchUiState.Success(results) }
        }
    }
}

Częste pytanie uzupełniające: dlaczego udostępniać asStateFlow() zamiast pola mutowalnego? Przekazuje ono interfejsowi widok tylko do odczytu, dzięki czemu stan może zmieniać się wyłącznie przez ViewModel, co zachowuje jednokierunkowy przepływ danych. Przewodnik Androida po StateFlow i SharedFlow zaleca dokładnie ten wzorzec.

SharedFlow vs StateFlow: wybór właściwego typu do zdarzeń

Klasyczne pytanie-pułapka brzmi: „czy StateFlow może dostarczać jednorazowe zdarzenia?". Szczera odpowiedź to nie, nie w bezpieczny sposób. Ponieważ StateFlow skleja i deduplikuje, zdarzenie nawigacji może zostać pominięte przy szybkich aktualizacjach, a przy zmianie konfiguracji uruchamia się ponownie, gdyż nowy kolektor odtwarza zachowaną wartość. SharedFlow z replay = 0 rozwiązuje oba problemy: nic nie jest zachowywane, a każda emisja dociera wyłącznie do kolektorów aktywnych w chwili emisji.

CheckoutViewModel.ktkotlin
class CheckoutViewModel : ViewModel() {
    // replay = 0: a late collector must NOT re-receive a past navigation event.
    private val _events = MutableSharedFlow<CheckoutEvent>(replay = 0)
    val events: SharedFlow<CheckoutEvent> = _events.asSharedFlow()

    fun onPaymentConfirmed() {
        viewModelScope.launch {
            // emit() suspends if the buffer is full; tryEmit() is the non-suspending variant.
            _events.emit(CheckoutEvent.NavigateToReceipt)
        }
    }
}

W pytaniach o architekturę głębsze uzasadnienie oddzielania stanu od zdarzeń pojawia się w porównaniu MVVM i MVI, gdzie MVI traktuje zdarzenia jako jawny strumień, a nie jako stan mutowalny.

Konwersja zimnego Flow na gorący StateFlow za pomocą stateIn

Rekruterzy często pytają, jak zamienić zimny flow z repozytorium na stan UI. Odpowiedzią jest stateIn (dla pojedynczej zachowywanej wartości) lub shareIn (dla rozgłaszania bez bieżącej wartości). Parametr SharingStarted kontroluje, kiedy strumień źródłowy jest aktywny, a WhileSubscribed(5_000) to standardowy wybór, ponieważ utrzymuje flow przy życiu podczas obrotu ekranu, nie powodując przy tym wycieku po zamknięciu ekranu.

ProfileViewModel.ktkotlin
val profile: StateFlow<Profile> = repo.profileStream()
    .stateIn(
        scope = viewModelScope,
        // Keep the upstream alive 5s after the last collector leaves, surviving rotation.
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = Profile.EMPTY
    )

Różnica między stateIn a shareIn to dokładnie różnica między StateFlow a SharedFlow: stateIn potrzebuje initialValue i przechowuje stan, natomiast shareIn przyjmuje liczbę replay i rozgłasza. Przegląd szerszego modelu korutyn w tym przewodniku po opanowaniu korutyn Kotlina pomaga powiązać te operatory z zakresami (scope) i anulowaniem.

Gotowy na rozmowy o Android?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Bezpieczne zbieranie flow w cyklu życia Androida

Pytanie na poziomie seniora dotyczy tego, jak zbierać flow bez powodowania wycieku pracy, gdy ekran znajduje się w tle. Zbieranie wewnątrz zwykłego launch działa dalej, gdy UI jest zatrzymane, marnując CPU i grożąc awariami. repeatOnLifecycle(STARTED) anuluje zbieranie przy STOP i wznawia je przy START.

ProfileFragment.kt (View system)kotlin
viewLifecycleOwner.lifecycleScope.launch {
    // Collection restarts on STARTED and cancels on STOPPED: no wasted work in the background.
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> render(state) }
    }
}

W Jetpack Compose odpowiednikiem jest collectAsStateWithLifecycle(), które przerywa zbieranie, gdy aplikacja trafia do tła, i wznawia je po powrocie.

ProfileScreen.kt (Jetpack Compose)kotlin
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    // collectAsStateWithLifecycle stops collecting when the app is backgrounded.
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(state)
}

Ta świadomość cyklu życia sprawia, że StateFlow w połączeniu z collectAsStateWithLifecycle jest domyślnym wzorcem obsługi stanu w nowoczesnych aplikacjach Compose, co szerzej omawiają te pytania rekrutacyjne o Jetpack Compose.

Jak testować emisje StateFlow i SharedFlow

To właśnie na testach seniorzy pokazują swoją przewagę. Naiwne podejście odczytuje stateFlow.value jeden raz, ale pomija stany pośrednie i w ogóle nie pozwala obserwować SharedFlow. Standardowa odpowiedź wskazuje bibliotekę Turbine, która zawiesza wykonanie do momentu nadejścia każdej emisji i oznacza test jako nieudany, jeśli oczekiwana wartość nigdy nie nadejdzie.

SearchViewModelTest.ktkotlin
@Test
fun `emits Loading then Success on query`() = runTest {
    val viewModel = SearchViewModel(fakeRepo)

    viewModel.uiState.test {
        assertEquals(SearchUiState.Idle, awaitItem())     // initial value
        viewModel.onQueryChanged("kotlin")
        assertEquals(SearchUiState.Loading, awaitItem())  // intermediate state
        assertEquals(SearchUiState.Success(fakeResults), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

Połączenie Turbine z runTest i testowym dyspozytorem sprawia, że asercje są deterministyczne, a wspomnienie różnicy między StandardTestDispatcher a UnconfinedTestDispatcher świadczy o realnym doświadczeniu w testowaniu korutyn.

Częste pytania uzupełniające o Kotlin Flow

Rekruterzy kończą serią szybkich pytań. Co robi SharingStarted.WhileSubscribed(5_000)? Uruchamia strumień źródłowy przy pierwszym subskrybencie i zatrzymuje go 5 sekund po odejściu ostatniego, co wystarcza, by przetrwać obrót ekranu. Dlaczego StateFlow pomija zduplikowane wartości? Porównuje je za pomocą equals(), więc emisja równej wartości nie robi nic, i dlatego klasy danych (data class) mają znaczenie dla stanu. Czy SharedFlow może zachowywać się jak StateFlow? Ustawienie replay = 1 sprawia, że zachowuje ostatnią wartość, ale nadal brakuje mu synchronicznego .value i nigdy nie deduplikuje. Co kontroluje przeciwciśnienie (back-pressure) w SharedFlow? Parametry extraBufferCapacity i onBufferOverflow, przy czym BufferOverflow.DROP_OLDEST to częsty wybór dla zdarzeń. Więcej praktyki z tymi operatorami znajduje się w module rekrutacyjnym o korutynach i Flow w Kotlinie.

Błąd, który przekreśla kandydatów

Używanie StateFlow do zdarzeń nawigacji lub snackbara. Ponieważ odtwarza on swoją ostatnią wartość nowym kolektorom, zdarzenie uruchamia się ponownie po obrocie ekranu i użytkownik zostaje przeniesiony dwukrotnie. Lepiej sięgnąć po SharedFlow z replay = 0 albo zamodelować jednorazowe zdarzenie, które jest czyszczone po obsłużeniu.

Warto przed rozmową przejrzeć źródła kotlinx.coroutines oraz ich dokumentację pakietu flow na GitHubie, ponieważ KDoc dla StateFlow i SharedFlow precyzyjnie opisuje gwarancje sklejania i odtwarzania.

Podsumowanie

Rozróżnienie Kotlin Flow vs StateFlow vs SharedFlow nagradza precyzyjny język na rozmowie kwalifikacyjnej. Najważniejsze wnioski do zapamiętania:

  • Flow jest zimny: producent uruchamia się ponownie dla każdego kolektora, więc pasuje do potoków danych działających na żądanie.
  • StateFlow jest gorący, zawsze przechowuje wartość oraz skleja i deduplikuje, co czyni go narzędziem do obsługi stanu UI.
  • SharedFlow jest gorący i nie wymaga wartości początkowej, co czyni go właściwym wyborem do jednorazowych zdarzeń.
  • Zamień zimny na gorący za pomocą stateIn lub shareIn i użyj SharingStarted.WhileSubscribed(5_000), aby przetrwać obrót ekranu.
  • Zbieraj za pomocą repeatOnLifecycle(STARTED) lub collectAsStateWithLifecycle(), aby uniknąć pracy w tle i wycieków.
  • Nigdy nie dostarczaj zdarzeń przez StateFlow: jego mechanizm odtwarzania uruchamia je ponownie po zmianie konfiguracji.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#android
#kotlin
#coroutines
#flow
#interview

Udostępnij

Powiązane artykuły