Usar corrotinas do Kotlin com componentes de arquitetura

As corrotinas de Kotlin fornecem uma API que permite criar um código assíncrono. Com elas, você pode definir um CoroutineScope, que ajuda a gerenciar quando as corrotinas precisam ser executadas. Cada operação assíncrona é executada em um escopo específico.

Os componentes de arquitetura fornecem compatibilidade de primeira classe com as corrotinas para escopos lógicos no seu app, assim como uma camada de interoperabilidade com LiveData. Este tópico explica como usar corrotinas de maneira eficiente com os componentes de arquitetura.

Adicionar dependências KTX

Os escopos de corrotina integrados descritos neste tópico estão contidos nas extensões KTX de cada componente de arquitetura correspondente. Adicione as dependências adequadas ao usar esses escopos.

  • Para ViewModelScope, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 ou posterior.
  • Para LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 ou posterior.
  • Para liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 ou posterior.

Escopos de corrotina com reconhecimento de ciclo de vida

Os componentes de arquitetura definem os seguintes escopos integrados que você pode usar no seu app.

ViewModelScope

Um ViewModelScope é definido para cada ViewModel no seu 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 para o ViewModel para que, se o ViewModel for apagado, o trabalho seja cancelado automaticamente para evitar o consumo de recursos.

Você pode acessar o CoroutineScope de um ViewModel por meio da propriedade viewModelScope do ViewModel, como 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 por meio das propriedades lifecycle.coroutineScope ou lifecycleOwner.lifecycleScope.

O exemplo abaixo demonstra como usar lifecycleOwner.lifecycleScope para criar texto pré-computado 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)
            }
        }
    }
    

Suspender corrotinas que reconhecem o ciclo de vida

Mesmo que o CoroutineScope forneça uma maneira adequada para cancelar operações de longa duração automaticamente, em outros casos, convém suspender a execução de um bloco de código, a menos que o Lifecycle esteja em um determinado estado. Por exemplo, para executar um FragmentTransaction, você precisa aguardar até que o Lifecycle esteja pelo menos no estado STARTED. Nesses casos, o 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 é executado somente 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 por meio de um dos métodos when, a corrotina será cancelada automaticamente. No exemplo abaixo, o bloco finally é executado assim que o estado do Lifecycle se torna 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.
                    }
                }
            }
        }
    }
    

Usar corrotinas com LiveData

Ao usar LiveData, talvez seja necessário calcular valores de forma assíncrona. Por exemplo, convém recuperar as preferências de um usuário e exibi-las na IU. Nesses casos, você pode usar a função do criador de 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 criador 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 uma simultaneidade estruturada primitiva (link em inglês) entre corrotinas e LiveData. O bloco de código começa a ser executado quando o LiveData se torna ativo e é automaticamente cancelado 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 algum outro motivo (por exemplo, geração de uma CancelationException), 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))
        }
    }
    

Você também pode combinar liveData com Transformations, como 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))
            }
        }
    }
    

Você pode 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: