Как работают SharedFlow и StateFlow и в чем их отличия
Что проверяют
Понимает ли кандидат, что оба потока горячие, чем replay-буфер отличается от хранения состояния и знает ли про conflation, дедупликацию по equals и потерю событий, когда подписчиков нет.
Как отвечать
Оба горячие: живут независимо от подписчиков, сами не завершаются и не перезапускают продюсера для каждого коллектора, в отличие от холодного flow {}.
SharedFlow это шина событий с настраиваемым буфером: replay, extraBufferCapacity, onBufferOverflow. По умолчанию replay = 0, и новый подписчик прошлых событий не увидит, но при replay = n увидит последние n значений. Формулировка «SharedFlow ничего не хранит» верна только для настроек по умолчанию.
Важная деталь про буфер: у MutableSharedFlow() без буфера emit приостанавливается, пока каждый текущий подписчик не обработает значение, а tryEmit в этой ситуации вернёт false. Если подписчиков нет вовсе и replay = 0, значение просто исчезает.
StateFlow это частный случай SharedFlow с replay = 1, обязательным начальным значением и conflation. Новый подписчик немедленно получает текущее value. Два следствия:
- Conflation: если значения ставятся быстрее, чем читаются, промежуточные теряются и доедет только последнее. Для состояния экрана это правильно, для событий это баг.
- Дедупликация по
equals: установка значения, равного текущему, подписчиков не будит. Поэтому изменение списка или data-класса на месте ломает обновление UI. Состояние держат неизменяемым и обновляют черезcopy.
Правило выбора: состояние экрана в StateFlow, одноразовые события (навигация, снекбар, ошибка) в SharedFlow с replay = 0 или в Channel(BUFFERED).receiveAsFlow().
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
val items: StateFlow<List<Item>> = repo.itemsFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())В UI собирать через repeatOnLifecycle(STARTED) или flowWithLifecycle, иначе коллектор продолжает работать, когда экран не виден. WhileSubscribed(5000) переживает поворот экрана, не перезапуская апстрим.
Где спотыкаются
Отдают одноразовые события через StateFlow и получают повторный показ диалога после поворота экрана. Второй частый случай: мутируют список внутри состояния и не понимают, почему UI не перерисовался, хотя причина в том, что equals вернул true.