March 21, 2023

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