Мы уже выяснили, что холодный Flow плохо дружит с Compose: при каждой рекомпозиции поток перезапускается с нуля. Решение — превратить его в горячий. Но как именно?
В Kotlin есть два оператора-подогревателя: stateIn() и shareIn(). Первый превращает холодный Flow в StateFlow, второй — в SharedFlow. Оба живут в скоупе, который вы им укажете, и делят данные между всеми подписчиками.
stateIn(): горячий поток с памятью
Типичный сценарий: репозиторий возвращает холодный поток, а ViewModel должен отдать интерфейсу горячий.
class CurrencyRepository {
fun getRateFlow(): Flow<Double> = flow {
while (true) {
val rate = fetchRate() // имитация запроса к API
emit(rate)
delay(30_000) // обновляем каждые 30 секунд
}
}
private suspend fun fetchRate(): Double {
delay(500) // имитация сети
return 89.5 + kotlin.random.Random.nextDouble(-2.0, 2.0)
}
}
Во ViewModel подогреваем:
class CurrencyViewModel : ViewModel() {
private val repository = CurrencyRepository()
val rate: StateFlow<Double> = repository
.getRateFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = 0.0
)
}
Разберём три параметра stateIn():
scope — скоуп, в котором будет жить горячий поток. Обычно это viewModelScope. Поток запустится в этом скоупе и будет отменён вместе с ним — когда ViewModel будет уничтожена.
initialValue — значение, которое StateFlow отдаст подписчику до первого реального обновления. В нашем случае — 0.0. Без начального значения первый подписчик получил бы… ничего, пока не завершится сетевой запрос.
started — самый интересный параметр. Он определяет стратегию жизни горячего потока.
Три стратегии SharingStarted
Eagerly — поток запускается немедленно при создании и работает до уничтожения скоупа. Даже если ни одного подписчика нет, данные всё равно генерируются. Подходит для данных, которые должны быть готовы мгновенно — например, настройки приложения или состояние авторизации.
Lazily — поток запускается при появлении первого подписчика и работает до уничтожения скоупа. Экономнее, чем Eagerly: если экран с курсом валют ни разу не открывался, запрос к API не выполнялся. Но после первого подписчика поток уже не остановится.
WhileSubscribed(stopTimeoutMillis) — самый популярный вариант. Поток активен, пока есть хотя бы один подписчик. Когда последний подписчик уходит, поток останавливается через stopTimeoutMillis миллисекунд. Если за это время появится новый подписчик — поток продолжит работу, не перезапускаясь.
Зачем нужна задержка? Чтобы не дёргать поток при кратковременных отписках. Поворот экрана — классический пример: старая Activity уничтожается, новая создаётся. Между этими событиями проходит доля секунды. Без задержки поток успеет остановиться и запуститься заново. С WhileSubscribed(5000) он спокойно переживёт паузу и продолжит вещание.
// Поток работает, пока есть подписчики + 5 секунд после последнего
SharingStarted.WhileSubscribed(5000)
// Поток работает всегда, даже без подписчиков
SharingStarted.Eagerly
// Поток запускается при первом подписчике и работает до конца
SharingStarted.Lazily
shareIn(): когда StateFlow — не то, что нужно
Оператор shareIn() работает по тому же принципу, но создаёт SharedFlow вместо StateFlow. Разница — в поведении:
StateFlow всегда хранит последнее значение и отдаёт его новому подписчику мгновенно. Если два одинаковых значения приходят подряд, второе игнорируется (distinctUntilChanged встроен).
SharedFlow по умолчанию ничего не хранит. Новый подписчик получит только те значения, которые придут после подписки. Зато дубликаты не фильтруются — каждый emit() доставляется как есть.
val events: SharedFlow<UiEvent> = repository
.getEventFlow()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
replay = 0 // не хранить предыдущие значения
)
Параметр replay у shareIn() определяет, сколько последних значений получит новый подписчик. При replay = 1 поведение приближается к StateFlow (но без фильтрации дубликатов).
Когда что выбрать
Если данные представляют состояние (текущий курс валют, имя пользователя, список товаров) — используйте stateIn(). Подписчик всегда получит актуальное значение.
Если данные представляют события (показать тост, навигация, одноразовые уведомления) — используйте shareIn(). Событие — это сигнал, а не состояние. Его не нужно хранить и показывать повторно.
На практике stateIn() встречается значительно чаще. Архитектура Compose построена вокруг состояния, и StateFlow идеально ложится в эту парадигму. shareIn() пригодится для специфических сценариев, где важна однократная доставка сигнала.
Резюме
Подогрев холодного потока — не прихоть, а архитектурная необходимость. Репозиторий создаёт данные. ViewModel подогревает их для UI. Composable подписывается и реагирует.
Repository (cold Flow)
↓ stateIn() / shareIn()
ViewModel (StateFlow / SharedFlow)
↓ collectAsStateWithLifecycle()
Composable (State<T>)
Этот конвейер — основа реактивной архитектуры Android-приложения.