Irbisa · Шпаргалка

Kotlin Flow

Холодные и горячие потоки, операторы, StateFlow и SharedFlow, Channel, flowOn, backpressure, stateIn и shareIn, обработка ошибок

Middle-разработчик · 20 вопросов

  1. 01

    Что такое Flow и чем холодный Flow отличается от горячего?

    Flow описывает асинхронный поток значений, построенный на suspend-функциях.

    val numbers = flow {  println("producer started")  emit(1)  emit(2)}…
  2. 02

    Чем Flow отличается от Sequence и почему внутри билдера sequence { } нельзя вызвать delay?

    Оба выдают значения лениво, по одному, но Sequence работает синхронно: потребитель вытягивает каждое значение через next(), а его поток выполняет продюсер до следующего yield.

    // Sequence: synchronous, the consumer pulls each valueval fib = sequence {  var a = 0  var b = 1  while (true) {    yield(a)                 // pauses until the next value is pulled…
  3. 03

    Какие операторы Flow терминальные, почему цепочка, которая заканчивается на onEach { }, ничего не делает, и что возвращает launchIn(scope)?

    Терминальные операторы запускают сбор: collect, first, toList, fold и подобные являются suspend-функциями, а launchIn собирает flow в новой корутине.

    val prices = flowOf(1, 2, 3)  .onEach { println("got $it") }  .map { it * 10 }                        // nothing runs yet: no terminal operator suspend fun demo(scope: CoroutineScope) {  println(prices.first())                 // got 1, 10: 2 and 3 are never produced…
  4. 04

    Чем StateFlow отличается от SharedFlow и когда использовать каждый?

    StateFlow нужен для стейта экрана: он всегда хранит текущее значение, требует начальное и пропускает равные значения.

    class CartVM : ViewModel() {  private val _state = MutableStateFlow(CartState())  val state: StateFlow<CartState> = _state.asStateFlow()   private val _messages = MutableSharedFlow<String>(extraBufferCapacity = 1)  val messages: SharedFlow<String> = _messages.asSharedFlow()…
  5. 05

    Найдите баг: после нажатия «Добавить» товар попадает в корзину, но список на экране не обновляется.

    class CartViewModel : ViewModel() {  private val items = mutableListOf<Item>()  private val _cart = MutableStateFlow<List<Item>>(items)  val cart: StateFlow<List<Item>> = _cart.asStateFlow()   fun add(item: Item) {    items.add(item)    _cart.value = items  }}

    StateFlow отбрасывает новое значение, равное текущему, а _cart.value = items присваивает тот же самый список, который уже в нём лежит.

  6. 06

    Найдите баг: экран показывает имя пользователя, но счётчик непрочитанных, который собирается строкой ниже, так и не появляется.

    class InboxFragment : Fragment(R.layout.fragment_inbox) {  private val vm: InboxViewModel by viewModels()   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {    val binding = FragmentInboxBinding.bind(view)    viewLifecycleOwner.lifecycleScope.launch {      viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {        vm.userName.collect { binding.name.text = it }          // StateFlow<String>        vm.unreadCount.collect { binding.badge.text = "$it" }   // StateFlow<Int>      }    }  }}

    collect на StateFlow никогда не возвращается, потому что горячий flow не завершается, и корутина застревает на первой строке: до второго collect дело не доходит.

  7. 07

    Что такое Channel, чем он отличается от Flow и что меняют для send ёмкости RENDEZVOUS, BUFFERED, CONFLATED и UNLIMITED?

    Channel представляет собой горячую очередь между корутинами: send кладёт элемент, receive забирает его, и каждый элемент достаётся ровно одному получателю.

    fun main() = runBlocking {  // A work queue: each job goes to exactly one worker  val jobs = Channel<Int>(Channel.BUFFERED)  repeat(2) { worker ->    launch {      for (job in jobs) {                 // ends when the channel is closed and drained…
  8. 08

    На каком потоке выполняется каждый пронумерованный шаг этого flow и как работает flowOn?

    // launched from viewModelScope (Dispatchers.Main)repo.rows()                          // flow { emit(db.readAll()) }  .map { it.toUi() }                 // (1)  .flowOn(Dispatchers.IO)  .filter { it.isNotEmpty() }        // (2)  .flowOn(Dispatchers.Default)  .collect { render(it) }            // (3)

    flowOn меняет контекст только для операторов выше себя, до предыдущего flowOn.

  9. 09

    Найдите баг: этот flow загружает две страницы параллельно и падает с IllegalStateException: Flow invariant is violated.

    fun feed(api: FeedApi): Flow<List<Post>> = flow {  coroutineScope {    launch { emit(api.page(1)) }   // each page in its own coroutine    launch { emit(api.page(2)) }  }}

    emit вызывается из корутин, запущенных через launch, а не из корутины, которая собирает flow, и flow { } это запрещает: «Emission from another coroutine is detected».

  10. 10

    Что выведет этот код с zip, а что с combine? Когда вместо них нужен merge?

    val numbers = flow { for (n in 1..3) { delay(100); emit(n) } }        // at 100, 200, 300 msval letters = flow { delay(150); emit("A"); delay(100); emit("B") }   // at 150, 250 ms fun main() = runBlocking {  numbers.zip(letters) { n, l -> "$n$l" }.collect { print("$it ") }  println()  numbers.combine(letters) { n, l -> "$n$l" }.collect { print("$it ") }}

    zip выводит 1A 2B: он составляет пары по позиции и останавливается, как только заканчивается один из flow, поэтому 3 остаётся без пары.

  11. 11

    flatMapConcat, flatMapMerge или flatMapLatest: что использовать для поиска по мере ввода и что ещё нужно в этом пайплайне?

    flatMapLatest: когда приходит новый запрос, он отменяет запрос для старого, и устаревшие результаты не могут перезаписать свежие.

    @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)class SearchVM(private val repo: SearchRepo) : ViewModel() {  private val query = MutableStateFlow("")  fun onQueryChange(text: String) { query.value = text }   val results: StateFlow<SearchState> = query…
  12. 12

    Чем отличаются debounce, sample и throttle-first и что из них подходит для строки поиска, потока с датчика и кнопки «Оплатить»?

    debounce(300) выдаёт значение, только если за 300 мс не пришло более нового, sample(300) раз в 300 мс выдаёт самое свежее значение, а throttle-first выдаёт первое значение и игнорирует остальные в течение окна.

    // kotlinx.coroutines has no throttleFirst: a few lines with flow { }fun <T> Flow<T>.throttleFirst(window: Duration): Flow<T> = flow {  var opened: TimeMark? = null              // per collector: state lives inside flow { }  collect { value ->    val mark = opened    if (mark == null || mark.elapsedNow() >= window) {…
  13. 13

    Flow эмитит быстрее, чем сборщик успевает обрабатывать. Что происходит по умолчанию и как это меняют buffer, conflate и collectLatest?

    По умолчанию ничего не теряется: emit приостанавливается, пока сборщик не обработает предыдущее значение, и продюсер замедляется до его темпа.

    val source = flow {  repeat(3) { delay(100); emit(it) }   // 100 ms to produce each} suspend fun compare() {  // default: about 900 ms, producer waits for the collector…
  14. 14

    Почему catch не видит исключение из блока collect и где в цепочке flow должны стоять retry и catch?

    catch обрабатывает только исключения апстрима (операторов выше себя), а исключение в collect { } находится ниже и уходит прямо в собирающую корутину.

    class FeedVM(private val repo: FeedRepo) : ViewModel() {  val feed: StateFlow<FeedState> = repo.feedFlow()    .retryWhen { cause, attempt ->      if (cause !is IOException || attempt >= 3) return@retryWhen false      delay(500L * (1 shl attempt.toInt()))          // 0.5 s, 1 s, 2 s      true                                           // re-collect the upstream…
  15. 15

    Как работают onStart, onCompletion и onEmpty и где сбрасывать флаг загрузки, чтобы он сбрасывался и при ошибке, и при отмене?

    onStart выполняется до запуска апстрима и может выдать первое значение, onEmpty срабатывает, когда апстрим завершился, не выдав ничего, а onCompletion выполняется после окончания flow по любой причине: успех, исключение или отмена.

    class SyncController(private val repo: SyncRepository, private val scope: CoroutineScope) {  val state = MutableStateFlow(SyncUiState())   fun start(): Job = repo.sync()                    // Flow<SyncProgress>, completes when done    .onStart { state.update { it.copy(isSyncing = true, error = null) } }    .onEach { p -> state.update { it.copy(percent = p.percent) } }…
  16. 16

    Когда функция репозитория должна возвращать Flow<T>, а когда быть suspend fun, которая возвращает T?

    Возвращайте Flow<T>, когда данные могут меняться, пока на них смотрят, и вызывающий должен видеть каждое изменение: Room-запрос, настройка в DataStore, сокет.

  17. 17

    Что делают stateIn и shareIn и что на самом деле контролирует SharingStarted.WhileSubscribed(5_000)?

    stateIn и shareIn один раз собирают холодный апстрим и раздают его многим подписчикам; SharingStarted решает, когда этот сбор начинается и заканчивается.

    class NotesVM(private val repo: NoteRepo) : ViewModel() {  // right: built once, one upstream collection for the ViewModel's life  val notes: StateFlow<List<Note>> = repo.notesFlow()    .stateIn(      scope = viewModelScope,      started = SharingStarted.WhileSubscribed(5_000),…
  18. 18

    Где должна стартовать первая загрузка экрана: в init { } ViewModel, в LaunchedEffect или во flow, отданном через stateIn? Чего стоит каждый вариант?

    Обычно внутри flow, отданного через stateIn(viewModelScope, WhileSubscribed(5_000), initial): загрузка стартует, когда экран впервые начинает сбор, переживает поворот и останавливается, когда никто не смотрит.

    class ProfileViewModel(private val repo: ProfileRepository) : ViewModel() {  private val retries = MutableStateFlow(0)   @OptIn(ExperimentalCoroutinesApi::class)  val state: StateFlow<ProfileUiState> = retries    .flatMapLatest {…
  19. 19

    Один медленный подписчик шины событий на MutableSharedFlow тормозит остальных подписчиков и эмиттер. Почему и что меняют extraBufferCapacity и onBufferOverflow?

    У SharedFlow один буфер на всех подписчиков, и слот освобождается, только когда его забрал самый медленный.

    fun main() = runBlocking {  val bus = MutableSharedFlow<Int>()             // replay 0, extraBufferCapacity 0  launch { bus.collect { delay(300); println("slow got $it") } }  launch { bus.collect { println("fast got $it") } }  delay(50)                                      // let both subscribe  repeat(4) { bus.emit(it) }                     // returns only after slow takes 3…
  20. 20

    Как Flow устроен под капотом (Flow, FlowCollector) и как написать собственный оператор?

    Flow является интерфейсом с одной функцией, suspend fun collect(collector: FlowCollector<T>), и у FlowCollector она тоже одна: suspend fun emit(value: T).

    // The whole contract, simplified from kotlinx.coroutines:// interface Flow<out T> { suspend fun collect(collector: FlowCollector<T>) }// fun interface FlowCollector<in T> { suspend fun emit(value: T) } // A custom operator: each value paired with the one before itfun <T : Any> Flow<T>.pairwise(): Flow<Pair<T, T>> = flow {…