Памятка: как подписываться на 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)
}
}