Используйте сопрограммы Kotlin с компонентами, учитывающими жизненный цикл (представлениями).

Концепции и реализация Jetpack Compose

Корутины Kotlin предоставляют API, позволяющий писать асинхронный код. С помощью корутин Kotlin вы можете определить CoroutineScope , который помогает управлять временем выполнения ваших корутин. Каждая асинхронная операция выполняется в рамках определенной области видимости.

Компоненты, учитывающие жизненный цикл, обеспечивают первоклассную поддержку сопрограмм для логических областей видимости в вашем приложении, а также уровень взаимодействия с LiveData . В этом разделе объясняется, как эффективно использовать сопрограммы с компонентами, учитывающими жизненный цикл.

Добавьте зависимости KTX

Встроенные области видимости сопрограмм, описанные в этом разделе, содержатся в расширениях KTX для каждого соответствующего компонента. Обязательно добавьте необходимые зависимости при использовании этих областей видимости.

  • Для ViewModelScope используйте androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 или более позднюю версию.
  • Для LifecycleScope используйте androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 или более позднюю версию.
  • Для liveData используйте androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 или более позднюю версию.

Сопрограммные области с учетом жизненного цикла

Компоненты, учитывающие жизненный цикл, определяют следующие встроенные области видимости, которые вы можете использовать в своем приложении.

ViewModelScope

Для каждой ViewModel в вашем приложении определяется область ViewModelScope . Любая сопрограмма, запущенная в этой области видимости, автоматически отменяется, если ViewModel очищается. Сопрограммы здесь полезны, когда вам нужно выполнить работу только тогда, когда ViewModel активна. Например, если вы вычисляете данные для макета, вам следует ограничить область видимости работы ViewModel , чтобы при очистке ViewModel работа автоматически отменялась во избежание потребления ресурсов.

Доступ CoroutineScope объекта ViewModel можно получить через свойство viewModelScope этого ViewModel , как показано в следующем примере:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

Область применения жизненного цикла

Для каждого объекта Lifecycle определяется область LifecycleScope . Любая сопрограмма, запущенная в этой области видимости, отменяется при уничтожении Lifecycle . Доступ к области CoroutineScope объекта Lifecycle можно получить либо через свойства lifecycle.coroutineScope , либо через свойства lifecycleOwner.lifecycleScope .

В приведенном ниже примере показано, как использовать lifecycleOwner.lifecycleScope для асинхронного создания предварительно вычисленного текста:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Перезапускаемые сопрограммы, учитывающие жизненный цикл

Несмотря на то, что lifecycleScope предоставляет надлежащий способ автоматической отмены длительных операций при завершении Lifecycle DESTROYED , могут возникнуть и другие ситуации, когда необходимо начать выполнение блока кода, когда Lifecycle находится в определенном состоянии, и отменить его, когда он перейдет в другое состояние. Например, может потребоваться сбор потока при STARTED Lifecycle (STARTED) и отмена сбора при его STOPPED ). Такой подход обрабатывает запросы на выполнение потока только тогда, когда пользовательский интерфейс виден на экране, экономя ресурсы и потенциально предотвращая сбои приложения.

В таких случаях Lifecycle и LifecycleOwner предоставляют API suspend repeatOnLifecycle , который делает именно это. В следующем примере содержится блок кода, который выполняется каждый раз, когда связанный Lifecycle находится как минимум в состоянии STARTED , и отменяется, когда Lifecycle STOPPED :

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Сбор данных о потоках с учетом жизненного цикла

Если вам нужно выполнить сбор данных с учетом жизненного цикла только для одного потока, вы можете использовать метод Flow.flowWithLifecycle() для упрощения кода:

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

Однако, если вам необходимо выполнять сборку данных с учетом жизненного цикла для нескольких потоков параллельно, то вам придется собирать данные для каждого потока в отдельных сопрограммах. В этом случае эффективнее использовать repeatOnLifecycle() напрямую:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

Приостановить сопрограммы, учитывающие жизненный цикл

Несмотря на то, что CoroutineScope предоставляет надлежащий способ автоматической отмены длительных операций, могут возникнуть и другие ситуации, когда необходимо приостановить выполнение блока кода, пока Lifecycle не достигнет определенного состояния. Например, для выполнения FragmentTransaction необходимо дождаться, пока Lifecycle не будет как минимум STARTED . Для таких случаев Lifecycle предоставляет дополнительные методы: lifecycle.whenCreated, lifecycle.whenStarted и lifecycle.whenResumed . Любая сопрограмма, выполняемая внутри этих блоков, приостанавливается, если Lifecycle не находится как минимум в минимально необходимом состоянии.

В приведенном ниже примере содержится блок кода, который выполняется только тогда, когда связанный с ним Lifecycle находится как минимум в состоянии STARTED :

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

Если состояние Lifecycle уничтожается во время активности сопрограммы с помощью одного из методов when , сопрограмма автоматически отменяется. В приведенном ниже примере блок finally выполняется после того, как состояние Lifecycle будет DESTROYED :

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

Используйте сопрограммы с LiveData

При использовании LiveData может потребоваться асинхронное вычисление значений. Например, вам может понадобиться получить настройки пользователя и отобразить их в пользовательском интерфейсе. В таких случаях вы можете использовать функцию построения liveData для вызова функции suspend , которая передаст результат в виде объекта LiveData .

В приведенном ниже примере функция loadUser() является функцией приостановки, объявленной в другом месте. Используйте функцию построения liveData для асинхронного вызова loadUser() , а затем используйте emit() для отправки результата:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

Блок liveData служит структурированным примитивом параллельного выполнения между сопрограммами и LiveData . Выполнение блока начинается, когда LiveData становится активным, и автоматически отменяется по истечении настраиваемого таймаута, когда LiveData становится неактивным. Если он отменяется до завершения, его перезапускается, если LiveData снова становится активным. Если он успешно завершился в предыдущем запуске, перезапуск не происходит. Обратите внимание, что перезапуск происходит только в случае автоматической отмены. Если блок отменяется по какой-либо другой причине (например, из-за исключения CancellationException ), он не перезапускается.

Вы также можете передавать несколько значений из блока. Каждый вызов emit() приостанавливает выполнение блока до тех пор, пока значение LiveData не будет установлено в основном потоке.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

Также можно комбинировать liveData с Transformations , как показано в следующем примере:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

Вы можете передавать несколько значений из LiveData , вызывая функцию emitSource() всякий раз, когда хотите передать новое значение. Обратите внимание, что каждый вызов emit() или emitSource() удаляет ранее добавленный источник.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

Для получения дополнительной информации о сопрограммах перейдите по следующим ссылкам:

Дополнительные ресурсы

Чтобы узнать больше об использовании сопрограмм с компонентами, учитывающими жизненный цикл, обратитесь к следующим дополнительным ресурсам.

Образцы

Блоги