На каком этапе жизненного цикла лучше подписываться и отписываться от Flow в Fragment и Activity
Что проверяют
Знаете ли вы, что у фрагмента два жизненных цикла, и понимаете ли разницу между «приостановить сбор» и «отменить сбор». Вопрос из тех, где устаревший ответ слышно с первой фразы.
Как отвечать
Современный ответ: не подписываться и отписываться руками в парных коллбэках, а привязать сбор к состоянию жизненного цикла.
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { render(it) }
}
}repeatOnLifecycle сам запускает сбор при переходе в состояние STARTED, отменяет корутину при уходе ниже, то есть на onStop, и запускает заново при возврате. STARTED выбирают потому, что это состояние видимости: невидимый экран обновлять незачем, а на RESUMED сбор рвался бы от любого диалога поверх.
Во фрагменте принципиален именно viewLifecycleOwner: у самого фрагмента жизненный цикл длиннее, чем у его view, и при возврате из бэкстека разметка пересоздаётся. В Activity такой развилки нет, там lifecycleScope самого компонента.
Со стороны ViewModel состояние отдают как StateFlow, собранный через stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initial). Таймаут переживает поворот экрана и не даёт апстриму перезапускаться на каждую смену конфигурации.
Где спотыкаются
Берут во фрагменте lifecycleScope вместо viewLifecycleOwner.lifecycleScope. Фрагмент вернулся из бэкстека, разметка новая, а старый сборщик жив и обращается к уничтоженной: утечка, лишние подписки и падение при обновлении UI.
Второе: пишут launchWhenStarted. Он лишь приостанавливает корутину, апстрим при этом продолжает работать, поэтому подписка на геолокацию или сокет остаётся активной в фоне и тратит батарею.
Третье: заводят собственный CoroutineScope и отменяют его руками. Отмену придётся продублировать во всех ветках выхода, и первая же забытая даёт живую корутину без экрана. Готовый scope делает это сам, а ручное управление оправдано только там, где сбор должен пережить экран, и тогда ему место в ViewModel.