Usar corrotinas do Kotlin com componentes que reconhecem o ciclo de vida

As corrotinas do Kotlin fornecem uma API que permite criar um 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 que reconhecem o ciclo de vida oferecem compatibilidade de primeira classe com as 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, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 ou versões mais recentes.
  • Para LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 ou versões mais recentes.
  • Para liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 ou 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 executado 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 (destruído), é possível que existam outros casos em que você precise iniciar a execução de um bloco de código quando 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 (iniciado) e cancelar a coleta quando ele for STOPPED (interrompido). Essa abordagem processa as emissões de fluxo apenas quando a IU 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 forma 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 Lifecycle esteja em 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 IU. Nesses casos, você pode usar a função do builder liveData para chamar uma função suspend, mostrando 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 bloco de criação liveData funciona como um primitivo de simultaneidade estruturada entre as 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 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:

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