Памятка: как подписываться на Kotlin Coroutines Flow с lifecycle-runtime-ktx версии 2.4.0 и выше
Введение
Рассмотрим поведение на примере с отображением некоторых данных на пользовательский интерфейс (UI).
class SampleFragment : BindingFragment(R.layout.fragment_sample) { override val binding by viewBinding<FragmentSampleBinding>() override val viewModel by viewModelFactory<SampleViewModel>() }
где функции viewBinding
и viewModelFactory
являются некоторыми PropertyDelegate.
Цель - перезапускать подписку на Flow
только в те моменты, когда пользовательский интерфейс виден. Использование функций launchWhen*
неподходит, так как корутина приостанавливает своё выполнение.
Fragment.onStart()
срабатывает, когдаFragment
становится видимым для пользователя без взаимодействия. Это хорошее место, чтобы начать рисовать визуальные элементы, запускать анимацию и т.д.Fragment.onStop()
вызывается, когдаFragment
перестает быть видимым для пользователя. Это хорошее место для прекращения обновления пользовательского интерфейса, отмены анимации и других визуальных действий.
Подписка на Flow для версии lifecycle-runtime-ktx ниже 2.4.0
Если требуется выполнить подписку только для одного Flow
, то достаточно вызвать Flow.collect()
в рамках начальной корутины uiChangesJob
:
import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch ... import androidx.lifecycle.lifecycleScope class SampleFragment : BindingFragment(R.layout.fragment_sample) { ... private var uiChangesJob: Job? = null override fun onStart() { super.onStart() uiChangesJob = viewLifecycleOwner.lifecycleScope.launch { // Делаем свою магию, например подписка viewModel.screenState .onEach { state -> render(state) } .collect() } } override fun onStop() { uiChangesJob?.cancel() super.onStop() } }
Однако если требуется выполнить подписку для нескольких Flow
параллельно, то необходимо подписываться на каждый Flow в разных корутинах. В этом случае эффективнее использовать Flow.launchIn()
:
uiChangesJob = viewLifecycleOwner.lifecycleScope.launch { viewModel.screenTitleFlow .onEach { text -> binding.titleView.text = text } .launchIn(this) viewModel.buttonStateFlow .onEach { isEnabled -> binding.buttonView.isEnabled = isEnabled } .launchIn(this) ... }
Что за viewLifecycleOwner.lifecycleScope
?
Начнём с того, что viewLifecycleOwner
- это LifecycleOwner
, который описывает жизненный цикл представления Fragment.onCreateView()
.
Далее LifecycleScope
- это CoroutineScope
жизненного цикла, который определен для каждого объекта Lifecycle
.
По умолчанию экземпляр LifecycleScope
создается только в момент доступа к свойству LifecycleOwner.lifecycleScope
.
Поскольку androidx.fragment.app.Fragment
является объектом Lifecycle
, то
- корутины объявленные во
Fragment.lifecycleScope
могут существовать после вызова первичного конструктораFragment()
и до вызоваFragment.onDestroy()
. - корутины объявленные во
Fragment.viewLifecycleOwner.lifecycleScope
могут существовать от вызоваFragment.onViewStateRestored()
до вызоваFragment.onDestroyView()
.
Lifecycle
и обратных вызовов Fragment
Для эффективного использования ресурсов устройства разработчику необходимо (но недостаточно) понимать тонкости работы жизненного цикла и особенностей Coroutines. Все зависит от целей использования, главное понимание границ!
Подписка на Flow для версии lifecycle-runtime-ktx 2.4.0 и выше
Тут все гораздо проще. Теперь API предоставляет специальную функцию, которая избавляет разработчика писать шаблонный код с подписками/отписками.
Данный поход является официальной рекомендацией к использованию.
Если требуется выполнить подписку только для одного Flow
, то достаточно вызвать Flow.flowWithLifecycle()
в рамках начальной корутины:
import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch ... import androidx.lifecycle.lifecycleScope class SampleFragment : BindingFragment(R.layout.fragment_sample) { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { viewModel.screenState .flowWithLifecycle(viewLifecycleOwner.lifecycle,Lifecycle.State.STARTED) .collect { state -> render(state) } } } }
Однако если требуется выполнить подписку для нескольких Flow
параллельно, то необходимо подписываться на каждый Flow
в разных корутинах. В этом случае эффективнее использовать LifecycleOwner.repeatOnLifecycle()
:
viewLifecycleOwner.lifecycleScope.launch { // Можно вставить код, который исполнится до состояния ЖЦ STARTED viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // repeatOnLifecycle запускает блок в новой короутине каждый раз, когда // жизненный цикл находится в состоянии STARTED (или выше) // и отменяет его, когда он STOPPED. viewModel.screenTitleFlow .onEach { text -> binding.titleView.text = text } .launchIn(this) viewModel.buttonStateFlow .onEach { isEnabled -> binding.buttonView.isEnabled = isEnabled } .launchIn(this) } }