A Visualização do desenvolvedor para Android 11 já está disponível. Teste e compartilhe seu feedback.

Melhorar o desempenho do app com corrotinas de Kotlin

Uma corrotina é um padrão de projeto de simultaneidade que você pode usar no Android para simplificar o código que é executado de forma assíncrona. Corrotinas foram adicionadas ao Kotlin na versão 1.3 e são baseadas em conceitos estabelecidos de outras linguagens.

No Android, as corrotinas ajudam a resolver dois problemas principais:

  • Gerenciar tarefas de longa duração que podem bloquear a linha de execução principal e congelar o app.
  • Fornecer a segurança principal ou chamar com segurança as operações de rede ou disco da linha de execução principal.

Este tópico descreve como você pode usar corrotinas do Kotlin para resolver esses problemas, permitindo criar um código de app mais simples e conciso.

Gerenciar tarefas de longa duração

No Android, cada app tem uma linha de execução principal que lida com a interface do usuário e gerencia as interações do usuário. Caso seu app esteja atribuindo muito trabalho à linha de execução principal, o app pode aparentemente congelar ou ficar significativamente lento. Solicitações de rede, análise de JSON, leitura ou gravação de um banco de dados ou até mesmo iteração em listas grandes podem fazer com que seu app seja executado lentamente o suficiente para causar instabilidade visível: IU lenta ou congelada que responde lentamente a eventos de toque. Essas operações de longa duração precisam ser executadas fora da linha de execução principal.

O exemplo a seguir mostra a implementação de corrotinas simples para uma tarefa hipotética de longa duração:

suspend fun fetchDocs() {                             // Dispatchers.Main
        val result = get("https://developer.android.com") // Dispatchers.IO for `get`
        show(result)                                      // Dispatchers.Main
    }

    suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
    

As corrotinas são criadas com base em funções regulares, adicionando duas operações para lidar com tarefas de longa duração. Além de invoke (ou call) e return, as corrotinas adicionam suspend e resume:

  • suspend pausa a execução da corrotina atual, salvando todas as variáveis locais.
  • resume continua a execução de uma corrotina suspensa do local onde ela foi suspensa.

É possível chamar funções suspend somente de outras funções suspend ou usando um builder de corrotinas, por exemplo, launch, para iniciar uma nova corrotina.

No exemplo acima, get() ainda é executado na linha de execução principal, mas suspende a corrotina antes de iniciar a solicitação de rede. Quando a solicitação de rede é concluída, get retoma a corrotina suspensa em vez de usar um callback para notificar a linha de execução principal.

O Kotlin usa um frame de pilha para gerenciar qual função está sendo executada com quaisquer variáveis locais. Ao suspender uma corrotina, o frame de pilha atual é copiado e salvo para mais tarde. Ao retomar, o frame de pilha é copiado de onde ele foi salvo, e a função começa a ser executada novamente. Mesmo que o código possa parecer uma solicitação de bloqueio sequencial comum, a corrotina garante que a solicitação de rede evite o bloqueio da linha de execução principal.

Usar corrotinas para a segurança principal

As corrotinas Kotlin usam dispatchers para determinar quais linhas de execução são usadas para a execução da corrotina. Para executar o código fora da linha de execução principal, você pode pedir para as corrotinas Kotlin realizarem o trabalho no dispatcher Padrão ou no de E/S. No Kotlin, todas as corrotinas precisam ser executadas em um dispatcher, mesmo quando estiverem em execução na linha de execução principal. As corrotinas podem suspender a si mesmas, e o dispatcher é responsável por reiniciá-las.

Para especificar onde as corrotinas precisam ser executadas, o Kotlin fornece três dispatchers que você pode usar:

  • Dispatchers.Main: use este dispatcher para executar uma corrotina na linha de execução principal do Android. Ele só deve ser usado para interagir com a IU e realizar um trabalho rápido. Exemplos incluem chamar funções suspend, executar operações de framework de IU do Android e atualizar objetos LiveData.
  • Dispatchers.IO: este dispatcher é otimizado para executar E/S de disco ou rede fora da linha de execução principal. Exemplos incluem uso do componente Room, leitura ou gravação de arquivos e execução de qualquer operação de rede.
  • Dispatchers.Default: este dispatcher é otimizado para realizar trabalho intensivo da CPU fora da linha de execução principal. Exemplos de casos de uso incluem classificação de uma lista e análise de JSON.

Continuando com o exemplo anterior, você pode usar os dispatchers para redefinir a função get. Dentro do corpo de get, chame withContext(Dispatchers.IO) para criar um bloco que é executado no pool de linhas de execução de E/S. Qualquer código que você inserir nesse bloco sempre será executado por meio do dispatcher IO. Como withContext é em si uma função de suspensão, a função get também é.

suspend fun fetchDocs() {                      // Dispatchers.Main
        val result = get("developer.android.com")  // Dispatchers.Main
        show(result)                               // Dispatchers.Main
    }

    suspend fun get(url: String) =                 // Dispatchers.Main
        withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
            /* perform network IO here */          // Dispatchers.IO (main-safety block)
        }                                          // Dispatchers.Main
    }
    

Com corrotinas, você pode enviar linhas de execução com controle refinado. Como withContext() permite controlar o pool de linhas de execução de qualquer linha de código sem introduzir callbacks, é possível aplicá-lo a funções muito pequenas, como ler um banco de dados ou executar uma solicitação de rede. Uma boa prática é usar withContext() para garantir que todas as funções sejam protegidas, o que significa que você pode chamar a função a partir da linha de execução principal. Dessa forma, o autor da chamada nunca precisa pensar sobre qual linha de execução tem que ser usada para executar a função.

No exemplo anterior, fetchDocs() é executado na linha de execução principal. No entanto, ele pode chamar com segurança get, que executa uma solicitação de rede em segundo plano. Como as corrotinas são compatíveis com suspend e resume, a corrotina na linha de execução principal é retomada com o resultado get assim que o bloco withContext é concluído.

Desempenho de withContext()

withContext() não adiciona mais sobrecarga em comparação com uma implementação equivalente baseada em callback. Além disso, é possível otimizar chamadas withContext() além de uma implementação equivalente baseada em callback em algumas situações. Por exemplo, se uma função faz dez chamadas para uma rede, você pode dizer à Kotlin para alternar as linhas de execução apenas uma vez usando um withContext() externo. Em seguida, mesmo que a biblioteca de rede use withContext() várias vezes, ela permanece no mesmo dispatcher e evita alternar linhas de execução. Além disso, o Kotlin otimiza a alternância entre Dispatchers.Default e Dispatchers.IO para evitar alternância de linhas de execução sempre que possível.

Designar um CoroutineScope

Ao definir uma corrotina, você também precisa designar seu CoroutineScope. Um CoroutineScope gerencia uma ou mais corrotinas relacionadas. Você também pode usar um CoroutineScope para iniciar uma nova corrotina nesse escopo. Diferentemente de um dispatcher, no entanto, um CoroutineScope não executa as corrotinas.

Uma função importante de CoroutineScope é interromper a execução da corrotina quando um usuário deixa uma área de conteúdo no seu app. Usando CoroutineScope, você pode garantir que todas as operações de execução sejam interrompidas corretamente.

Usar o CoroutineScope com componentes da arquitetura do Android

No Android, é possível associar implementações de CoroutineScope ao ciclo de vida de um componente. Isso permite que você evite vazamentos de memória ou faça trabalhos extras para atividades ou fragmentos que não são mais relevantes para o usuário. Usando componentes do Jetpack, eles se encaixam naturalmente em um ViewModel. Como ViewModel não é destruído durante alterações de configuração (como rotação de tela), você não precisa se preocupar com o cancelamento ou a reinicialização das corrotinas.

Os escopos reconhecem todas as corrotinas iniciadas por eles. Isso significa que você pode cancelar tudo que foi iniciado no escopo a qualquer momento. Os escopos se propagam; portanto, se uma corrotina iniciar outra corrotina, ambas terão o mesmo escopo. Isso significa que, mesmo que outras bibliotecas iniciem uma corrotina a partir do seu escopo, você poderá cancelá-las a qualquer momento. Isso é particularmente importante se você estiver executando corrotinas em um ViewModel. Caso seu ViewModel esteja sendo destruído porque o usuário saiu da tela, todo o trabalho assíncrono que ele estiver fazendo precisa ser interrompido. Caso contrário, você desperdiçará recursos e possivelmente vazará memória. Se você tiver um trabalho assíncrono que tenha que continuar depois de destruir ViewModel, ele precisará ser feito em uma camada inferior da arquitetura do seu app.

Com a biblioteca KTX para os componentes da arquitetura do Android, você também pode usar uma propriedade de extensão, viewModelScope, para criar corrotinas que podem ser executadas até ViewModel ser destruído.

Iniciar uma corrotina

Você pode iniciar corrotinas de duas maneiras:

  • launch inicia uma nova corrotina e não retorna o resultado para o autor da chamada. Qualquer trabalho que seja considerado "disparar e esquecer" pode ser iniciado usando launch.
  • async inicia uma nova corrotina e permite retornar um result com uma função de suspensão chamada await.

Normalmente, você precisa launch uma nova corrotina de uma função regular, já que uma função regular não pode chamar await. Use async somente dentro de outra corrotina ou dentro de uma função de suspensão e ao realizar a decomposição paralela.

Com base nos exemplos anteriores, veja uma corrotina com a extensão viewModelScope da propriedade da extensão KTX que usa launch para alternar entre funções regulares e corrotinas:

fun onDocsNeeded() {
        viewModelScope.launch {    // Dispatchers.Main
            fetchDocs()            // Dispatchers.Main (suspend function call)
        }
    }
    

Decomposição paralela

Como todas as corrotinas iniciadas por uma função suspend precisam ser interrompidas quando a função for retornada, você provavelmente precisará garantir que essas corrotinas terminem antes de retornar. Com simultaneidade estruturada em Kotlin, você pode definir um coroutineScope que inicie uma ou mais corrotinas. Em seguida, usando await() (para uma corrotina única) ou awaitAll() (para diversas corrotinas), você pode garantir que essas corrotinas terminem antes de retornar da função.

Como exemplo, vamos definir um coroutineScope que busque dois documentos de forma assíncrona. Chamando await() em cada referência adiada, garantimos que as duas operações async terminem antes de retornar um valor:

suspend fun fetchTwoDocs() =
        coroutineScope {
            val deferredOne = async { fetchDoc(1) }
            val deferredTwo = async { fetchDoc(2) }
            deferredOne.await()
            deferredTwo.await()
        }
    

Você também pode usar awaitAll() nas coleções, conforme mostrado neste exemplo:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
        coroutineScope {
            val deferreds = listOf(     // fetch two docs at the same time
                async { fetchDoc(1) },  // async returns a result for the first doc
                async { fetchDoc(2) }   // async returns a result for the second doc
            )
            deferreds.awaitAll()        // use awaitAll to wait for both network requests
        }
    

Mesmo que fetchTwoDocs() inicie novas corrotinas com async, a função usa awaitAll() para aguardar até que as corrotinas sejam concluídas antes de retornar. Observe, no entanto, que mesmo se não tivéssemos chamado awaitAll(), o builder coroutineScope não retomaria a corrotina que chamou fetchTwoDocs até que todas as novas corrotinas fossem concluídas.

Além disso, coroutineScope detecta exceções que as corrotinas lançam e as encaminha de volta para o autor da chamada.

Para saber mais sobre decomposição paralela, consulte Como escrever funções de suspensão (link em inglês).

Componentes de arquitetura com suporte integrado

Alguns componentes da arquitetura, incluindo ViewModel e Lifecycle, incluem suporte integrado para corrotinas por meio dos próprios membros CoroutineScope.

Por exemplo, ViewModel inclui um viewModelScope integrado. Isso fornece uma maneira padrão de iniciar corrotinas dentro do escopo de ViewModel, conforme mostrado neste exemplo.

class MyViewModel : ViewModel() {

        fun launchDataLoad() {
            viewModelScope.launch {
                sortList()
                // Modify UI
            }
        }

        /**
        * Heavy operation that cannot be done in the Main Thread
        */
        suspend fun sortList() = withContext(Dispatchers.Default) {
            // Heavy work
        }
    }
    

LiveData também utiliza corrotinas com um bloco liveData:

liveData {
        // runs in its own LiveData-specific scope
    }
    

Para saber mais sobre componentes de arquitetura com suporte de corrotinas integrado, consulte Usar corrotinas Kotlin com componentes de arquitetura.

Mais informações

Para mais informações relacionadas a corrotinas, consulte os seguintes links: