Conceitos e implementação do Jetpack Compose
As corrotinas do Kotlin fornecem uma API que permite escrever código
assíncrono. Com elas, é possível definir um CoroutineScope, que
ajuda a gerenciar quando as corrotinas serão executadas. Cada operação assíncrona
é executada em um escopo específico.
Os componentes com reconhecimento de ciclo de vida oferecem suporte de primeira classe a corrotinas para
escopos lógicos no seu app, além de uma camada de interoperabilidade com
LiveData. Este tópico explica como usar corrotinas de maneira eficiente com componentes
que reconhecem o ciclo de vida.
Adicionar dependências KTX
Os escopos de corrotina integrados descritos neste tópico estão contidos nas extensões KTX de cada componente correspondente. Adicione as dependências adequadas ao usar esses escopos.
- Para
ViewModelScope, useandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0ou versões mais recentes. - Para
LifecycleScope, useandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0ou versões mais recentes. - Para
liveData, useandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0ou versões mais recentes.
Escopos de corrotina com reconhecimento de ciclo de vida
Os componentes com reconhecimento de ciclo de vida definem os seguintes escopos integrados que você pode usar no seu app.
ViewModelScope
Um ViewModelScope é definido para cada ViewModel no app. Qualquer
corrotina iniciada nesse escopo será cancelada automaticamente se o ViewModel for
apagado. As corrotinas são úteis aqui quando você tem um trabalho que precisa ser feito
somente se o ViewModel estiver ativo. Por exemplo, se você estiver computando alguns dados
de um layout, será necessário definir o escopo do trabalho como ViewModel. Dessa forma, o trabalho será cancelado automaticamente para evitar o consumo
de recursos caso o ViewModel seja apagado.
É possível acessar o CoroutineScope de um ViewModel pela
propriedade viewModelScope do ViewModel, conforme mostrado no exemplo a seguir:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
Um LifecycleScope é definido para cada objeto Lifecycle. Qualquer corrotina
iniciada nesse escopo será cancelada quando o Lifecycle for destruído. Você pode
acessar o CoroutineScope do Lifecycle usando as
propriedades lifecycle.coroutineScope ou lifecycleOwner.lifecycleScope.
O exemplo abaixo demonstra como usar o lifecycleOwner.lifecycleScope para
criar textos pré-computados de forma assíncrona:
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)
}
}
}
Corrotinas reinicializáveis que reconhecem o ciclo de vida
Mesmo que o lifecycleScope ofereça uma maneira adequada de cancelar operações de longa duração automaticamente quando o Lifecycle for DESTROYED, é possível que existam outros casos em que você precise iniciar a execução de um bloco de código quando o Lifecycle estiver em um determinado estado e cancelá-la quando ele estiver em outro estado. Por
exemplo, você pode querer coletar um fluxo quando o Lifecycle for STARTED e
cancelar a coleta quando ele for STOPPED. Essa abordagem processa as emissões de fluxo apenas quando a interface está visível na tela, economizando recursos e possivelmente evitando falhas no app.
Para esses casos, Lifecycle e LifecycleOwner fornecem a API repeatOnLifecycle
de suspensão que faz exatamente isso. O exemplo abaixo contém um
bloco de código que é executado sempre que o Lifecycle associado está pelo menos no
estado STARTED e é cancelado quando o Lifecycle é STOPPED (interrompido):
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
}
}
}
}
}
Coleta de fluxo com reconhecimento de ciclo de vida
Se você precisar fazer a coleta com reconhecimento de ciclo de vida em um único fluxo, use o método Flow.flowWithLifecycle() para simplificar seu código:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
Se precisar fazer em vários fluxos em
paralelo, colete cada um deles em corrotinas diferentes. Nesse caso,
é mais eficiente usar repeatOnLifecycle() diretamente:
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. */ }
}
}
}
Suspender corrotinas que reconhecem o ciclo de vida
Mesmo que o CoroutineScope forneça uma maneira adequada de cancelar operações de longa duração automaticamente, é possível que existam outros casos em que você precise suspender a execução de um bloco de código, a menos que o Lifecycle esteja em um determinado estado. Por exemplo, para executar uma FragmentTransaction, é necessário aguardar até que o Lifecycle seja pelo menos STARTED. Para esses casos, Lifecycle fornece outros métodos: lifecycle.whenCreated, lifecycle.whenStarted e lifecycle.whenResumed. Qualquer
execução de corrotina dentro desses blocos será suspensa se o Lifecycle não estiver no estado mínimo desejado.
O exemplo abaixo contém um bloco de código que só é executado quando o Lifecycle associado
está pelo menos no estado 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.
}
}
}
Se o Lifecycle for destruído enquanto uma corrotina estiver ativa usando um dos métodos
when, a corrotina será cancelada automaticamente. No exemplo abaixo,
o bloco finally é executado quando o estado Lifecycle é DESTROYED (destruído):
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.
}
}
}
}
}
Usar corrotinas com LiveData
Ao usar LiveData, pode ser necessário calcular valores de forma assíncrona.
Por exemplo, caso você queira recuperar as preferências de um usuário e exibi-las na interface. Nesses casos, você pode usar a função do builder liveData para chamar uma
função suspend, exibindo o resultado como um objeto LiveData.
No exemplo abaixo, loadUser() é uma função de suspensão declarada em outro lugar. Use
a função do builder liveData para chamar loadUser() de forma assíncrona e, em seguida,
use emit() para emitir o resultado:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
O liveData elemento básico funciona como um primitivo de simultaneidade estruturada entre corrotinas e LiveData. O bloco de código
começa a ser executado quando LiveData fica ativo e é cancelado automaticamente
após um tempo limite configurável quando o LiveData fica inativo. Se for cancelado antes da conclusão, ele será reiniciado caso o LiveData fique ativo novamente. Se tiver sido concluído com êxito em uma execução anterior, ele não será reiniciado. Ele só será reiniciado se for cancelado automaticamente. Se o bloco for cancelado
por qualquer outro motivo (por exemplo, geração de uma CancellationException), ele não
será reiniciado.
Também é possível emitir vários valores a partir do bloco. Cada chamada emit() suspende
a execução do bloco até que o valor LiveData seja definido na linha de execução principal.
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
Também é possível combinar liveData com Transformations, conforme mostrado no
exemplo a seguir:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
É possível emitir vários valores de um LiveData chamando a função emitSource()
sempre que quiser emitir um novo valor. Cada chamada para emit()
ou emitSource() remove a origem adicionada anteriormente.
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)
}
)
}
}
}
Para mais informações relacionadas a corrotinas, consulte os seguintes links:
- Melhorar o desempenho do app com corrotinas de Kotlin
- Visão geral de corrotinas (link em inglês)
- Linhas de execução no CoroutineWorker
Outros recursos
Para saber mais sobre o uso de corrotinas com componentes que reconhecem o ciclo de vida, consulte os recursos a seguir.
Exemplos
Blogs
- Corrotinas no Android: padrões de aplicativos
- Corrotinas fáceis no Android: viewModelScope
- Como testar duas emissões de LiveData consecutivas em corrotinas