Neste codelab, você vai aprender a usar as corrotinas do Kotlin em um app Android, uma nova maneira de gerenciar linhas de execução em segundo plano que pode simplificar o código, reduzindo a necessidade de callbacks. As corrotinas são um recurso do Kotlin que converte callbacks assíncronos para tarefas de longa duração, como acesso a banco de dados ou rede, em código sequencial.
Confira um snippet de código para ter uma ideia do que você vai fazer.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
O código baseado em callback será convertido em código sequencial usando corrotinas.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Você vai começar com um app já existente, criado com os Componentes de arquitetura, que usa um estilo de callback para tarefas de longa duração.
Ao final deste codelab, você terá experiência suficiente para usar corrotinas no seu app para carregar dados da rede e integrar corrotinas a um app. Você também vai conhecer as práticas recomendadas para corrotinas e como escrever um teste em relação ao código que usa corrotinas.
Pré-requisitos
- Familiaridade com os componentes de arquitetura
ViewModel
,LiveData
,Repository
eRoom
. - Experiência com sintaxe do Kotlin, incluindo funções de extensão e lambdas.
- Conhecimentos básicos sobre o uso de linhas de execução no Android, incluindo a linha de execução principal, linhas de execução em segundo plano e callbacks
O que você aprenderá
- Chamar o código escrito com corrotinas e receber resultados.
- Use funções de suspensão para tornar o código assíncrono sequencial.
- Use
launch
erunBlocking
para controlar como o código é executado. - Aprenda técnicas para converter APIs atuais em corrotinas usando
suspendCoroutine
. - Usar corrotinas com componentes de arquitetura.
- Conheça as práticas recomendadas para testar corrotinas.
O que é necessário
- Android Studio 3.5. O codelab pode funcionar com outras versões, mas alguns recursos podem estar ausentes ou ser diferentes.
Se você encontrar algum problema (bugs no código, erros gramaticais, instruções pouco claras, etc.) neste codelab, informe o problema no link Informar um erro no canto inferior esquerdo do codelab.
Baixar o código
Clique no link abaixo para fazer o download de todo o código para este codelab:
… ou clone o repositório do GitHub pela linha de comando usando o seguinte comando:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Perguntas frequentes
Primeiro, vamos ver a aparência do app de exemplo inicial. Siga estas instruções para abrir o app de exemplo no Android Studio.
- Se você fez o download do arquivo ZIP
kotlin-coroutines
, descompacte-o. - Abra o projeto
coroutines-codelab
no Android Studio. - Selecione o módulo de aplicativo
start
. - Clique no botão
Run e escolha um emulador ou conecte seu dispositivo Android, que precisa ser capaz de executar o Android Lollipop. O SDK mínimo compatível é o 21. A tela "Corrotinas do Kotlin" vai aparecer:
Esse app inicial usa linhas de execução para incrementar a contagem com um pequeno atraso depois que você pressiona a tela. Ele também vai buscar um novo título na rede e mostrar na tela. Tente agora. A contagem e a mensagem vão mudar após um pequeno atraso. Neste codelab, você vai converter esse aplicativo para usar corrotinas.
Esse app usa componentes de arquitetura para separar o código da interface em MainActivity
da lógica do aplicativo em MainViewModel
. Familiarize-se com a estrutura do projeto.
MainActivity
mostra a interface, registra listeners de clique e pode exibir umSnackbar
. Ele transmite eventos paraMainViewModel
e atualiza a tela com base emLiveData
emMainViewModel
.- O
MainViewModel
processa eventos emonMainViewClicked
e se comunica comMainActivity
usandoLiveData.
. - O
Executors
define oBACKGROUND,
, que pode executar ações em uma linha de execução em segundo plano. - O
TitleRepository
busca resultados da rede e os salva no banco de dados.
Como adicionar corrotinas a um projeto
Para usar corrotinas em Kotlin, inclua a biblioteca coroutines-core
no arquivo build.gradle (Module: app)
do seu projeto. Os projetos do codelab já fizeram isso para você, então não é necessário fazer isso para concluir o codelab.
As corrotinas no Android estão disponíveis como uma biblioteca principal e extensões específicas do Android:
- kotlinx-corountines-core : interface principal para usar corrotinas em Kotlin.
- kotlinx-coroutines-android : suporte à linha de execução principal do Android em corrotinas.
O app inicial já inclui as dependências em build.gradle.
. Ao criar um novo projeto de app, abra build.gradle (Module: app)
e adicione as dependências de corrotinas ao projeto.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
No Android, é essencial evitar o bloqueio da linha de execução principal. A linha de execução principal é uma única linha que processa todas as atualizações na interface. Além disso, é a linha de execução que chama todos os gerenciadores de clique e outros callbacks da interface. Por isso, ela precisa ser executada sem problemas para garantir uma ótima experiência do usuário.
Para que o app seja mostrado sem pausas visíveis, a linha de execução principal precisa atualizar a tela a cada 16 ms ou mais, o que equivale a cerca de 60 quadros por segundo. Muitas tarefas comuns levam mais tempo, como analisar grandes conjuntos de dados JSON, gravar dados em um banco de dados ou buscar dados da rede. Portanto, chamar um código como esse da linha de execução principal pode fazer com que o app seja interrompido, atrasar a renderização ou até causar travamentos. Além disso, se você bloquear a linha de execução principal por muito tempo, poderá até ocorrer um erro, que será acompanhado de uma caixa de diálogo O app não está respondendo.
Assista ao vídeo abaixo para saber como as corrotinas resolvem esse problema no Android ao introduzir a segurança da linha de execução principal.
O padrão de callback
Um padrão para realizar tarefas de longa duração sem bloquear a linha de execução principal é o uso de callbacks. Com callbacks, você pode iniciar tarefas de longa duração em uma linha de execução em segundo plano. Quando a tarefa é concluída, o callback é chamado para informar o resultado na linha de execução principal.
Confira um exemplo do padrão de callback.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Como esse código é anotado com @UiThread
, ele precisa ser executado rápido o suficiente para ser executado na linha de execução principal. Isso significa que ele precisa retornar muito rápido para que a próxima atualização da tela não seja atrasada. No entanto, como slowFetch
leva segundos ou até minutos para ser concluído, a linha de execução principal não pode esperar o resultado. O callback show(result)
permite que slowFetch
seja executado em uma linha de execução em segundo plano e retorne o resultado quando estiver pronto.
Usar corrotinas para remover callbacks
Os callbacks são um ótimo padrão, mas eles têm algumas desvantagens. Códigos que usam muitos callbacks podem ser difíceis de ler e mais difíceis de entender. Além disso, callbacks não permitem o uso de alguns recursos de linguagem, como exceções.
As corrotinas do Kotlin permitem converter códigos baseados em callback em códigos sequenciais. Geralmente, é mais fácil ler um código programado de maneira sequencial. Além disso, ele também pode usar recursos de linguagem, como exceções.
No fim, eles fazem exatamente a mesma coisa: esperam até que um resultado esteja disponível em uma tarefa de longa duração e continuam a execução. No entanto, no código, eles parecem muito diferentes.
A palavra-chave suspend
é a maneira do Kotlin de marcar uma função ou um tipo de função disponível para corrotinas. Quando uma corrotina chama uma função marcada como suspend
, em vez de bloquear até que essa função seja retornada como uma chamada de função normal, ela suspende a execução até que o resultado esteja pronto e, em seguida, retoma de onde parou com o resultado. Enquanto está suspensa aguardando um resultado, ela desbloqueia a linha de execução em que está sendo executada para que outras funções ou corrotinas possam ser executadas.
Por exemplo, no código abaixo, makeNetworkRequest()
e slowFetch()
são funções suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Assim como na versão de callback, makeNetworkRequest
precisa retornar da linha de execução principal imediatamente porque está marcado como @UiThread
. Isso significa que geralmente não é possível chamar métodos de bloqueio como slowFetch
. É aqui que a palavra-chave suspend
faz sua mágica.
Em comparação com o código baseado em callback, o código de corrotina alcança o mesmo resultado de desbloquear a linha de execução atual com menos código. Devido ao estilo sequencial, é fácil encadear várias tarefas de longa duração sem criar vários callbacks. Por exemplo, o código que busca um resultado de dois endpoints de rede e o salva no banco de dados pode ser escrito como uma função em corrotinas sem callbacks. Assim:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
Você vai incluir corrotinas no app de exemplo na próxima seção.
Neste exercício, você vai escrever uma corrotina para mostrar uma mensagem após um atraso. Para começar, verifique se o módulo start
está aberto no Android Studio.
Noções básicas sobre CoroutineScope
Em Kotlin, todas as corrotinas são executadas em um CoroutineScope
. Um escopo controla o ciclo de vida das corrotinas com o job. Quando você cancelar o job de um escopo, todas as corrotinas iniciadas nesse escopo serão canceladas. No Android, você pode usar um escopo para cancelar todas as corrotinas em execução quando, por exemplo, o usuário sai de um Activity
ou Fragment
. Os escopos também permitem especificar um dispatcher padrão. Um dispatcher controla qual linha de execução executa uma corrotina.
Para corrotinas iniciadas pela interface, geralmente é correto iniciá-las em Dispatchers.Main
, que é a linha de execução principal no Android. Uma corrotina iniciada em Dispatchers.Main
não bloqueia a linha de execução principal enquanto está suspensa. Como uma corrotina ViewModel
quase sempre atualiza a interface na linha de execução principal, iniciar corrotinas nessa linha economiza trocas extras de linhas de execução. Uma corrotina iniciada na linha de execução principal pode alternar os agentes a qualquer momento depois de iniciada. Por exemplo, ele pode usar outro dispatcher para analisar um grande resultado JSON fora da linha de execução principal.
Usar viewModelScope
A biblioteca AndroidX lifecycle-viewmodel-ktx
adiciona um CoroutineScope aos ViewModels configurado para iniciar corrotinas relacionadas à interface. Para usar essa biblioteca, inclua-a no arquivo build.gradle (Module: start)
do projeto. Essa etapa já foi feita nos projetos do codelab.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
A biblioteca adiciona um viewModelScope
como uma função de extensão da classe ViewModel
. Esse escopo está vinculado a Dispatchers.Main
e será cancelado automaticamente quando o ViewModel
for liberado.
Mudar de linhas de execução para corrotinas
Em MainViewModel.kt
, encontre o próximo TODO com este código:
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
Esse código usa o BACKGROUND ExecutorService
(definido em util/Executor.kt
) para ser executado em uma linha de execução em segundo plano. Como sleep
bloqueia a linha de execução atual, ele congelaria a interface se fosse chamado na linha de execução principal. Um segundo depois que o usuário clica na visualização principal, ela solicita uma snackbar.
Para ver isso acontecer, remova o BACKGROUND do código e execute-o novamente. O ícone de carregamento não vai aparecer, e tudo vai "pular" para o estado final um segundo depois.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Substitua updateTaps
por este código baseado em corrotina que faz a mesma coisa. Você precisará importar launch
e delay
.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Esse código faz a mesma coisa, esperando um segundo antes de mostrar uma snackbar. No entanto, há algumas diferenças importantes:
viewModelScope.
launch
vai iniciar uma corrotina noviewModelScope
. Isso significa que, quando o job transmitido paraviewModelScope
for cancelado, todas as corrotinas nesse job/escopo também serão canceladas. Se o usuário sair da atividade antes quedelay
retorne, essa corrotina será cancelada automaticamente quandoonCleared
for chamada após a destruição do ViewModel.- Como
viewModelScope
tem um dispatcher padrão deDispatchers.Main
, essa corrotina será iniciada na linha de execução principal. Vamos ver mais tarde como usar diferentes linhas de execução. - A função
delay
é uma funçãosuspend
. Isso é mostrado no Android Studio pelo íconena margem esquerda. Mesmo que essa corrotina seja executada na linha de execução principal,
delay
não vai bloquear a linha por um segundo. Em vez disso, o dispatcher vai programar a retomada da corrotina em um segundo na próxima instrução.
Continue e execute o teste. Ao clicar na visualização principal, uma snackbar vai aparecer um segundo depois.
Na próxima seção, vamos considerar como testar essa função.
Neste exercício, você vai escrever um teste para o código que acabou de criar. Neste exercício, mostramos como testar corrotinas em execução no Dispatchers.Main
usando a biblioteca kotlinx-coroutines-test. Mais adiante neste codelab, você vai implementar um teste que interage diretamente com as corrotinas.
Revise o código atual
Abra MainViewModelTest.kt
na pasta androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Uma regra é uma maneira de executar código antes e depois da execução de um teste no JUnit. Duas regras são usadas para testar MainViewModel em um teste fora do dispositivo:
InstantTaskExecutorRule
é uma regra do JUnit que configura oLiveData
para executar cada tarefa de forma síncrona.MainCoroutineScopeRule
é uma regra personalizada nesta base de código que configuraDispatchers.Main
para usar umTestCoroutineDispatcher
dekotlinx-coroutines-test
. Isso permite que os testes avancem um relógio virtual para testes e que o código useDispatchers.Main
em testes de unidade.
No método setup
, uma nova instância de MainViewModel
é criada usando simulações de teste. Essas são implementações falsas da rede e do banco de dados fornecidas no código inicial para ajudar a escrever testes sem usar a rede ou o banco de dados real.
Para esse teste, os objetos simulados são necessários apenas para atender às dependências de MainViewModel
. Mais adiante neste codelab, você vai atualizar os fakes para oferecer suporte a corrotinas.
Escrever um teste que controla corrotinas
Adicione um novo teste que garanta que os toques sejam atualizados um segundo depois que a visualização principal for clicada:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Ao chamar onMainViewClicked
, a corrotina que acabamos de criar será iniciada. Esse teste verifica se o texto de toques permanece "0 toques" logo após a chamada de onMainViewClicked
e se é atualizado para "1 toque" um segundo depois.
Esse teste usa o tempo virtual para controlar a execução da corrotina iniciada por onMainViewClicked
. O MainCoroutineScopeRule
permite pausar, retomar ou controlar a execução de corrotinas iniciadas no Dispatchers.Main
. Aqui, estamos chamando advanceTimeBy(1_000)
, o que faz com que o agente principal execute imediatamente as corrotinas programadas para serem retomadas um segundo depois.
Esse teste é totalmente determinístico, o que significa que ele sempre será executado da mesma forma. Além disso, como ele tem controle total sobre a execução de corrotinas iniciadas no Dispatchers.Main
, não é necessário esperar um segundo para que o valor seja definido.
Executar o teste atual
- Clique com o botão direito do mouse no nome da classe
MainViewModelTest
no editor para abrir um menu de contexto. - No menu de contexto, escolha
Executar "MainViewModelTest".
- Para execuções futuras, selecione essa configuração de teste nas configurações ao lado do botão
na barra de ferramentas. Por padrão, a configuração será chamada de MainViewModelTest.
O teste vai aparecer como aprovado. E a execução leva bem menos de um segundo.
No próximo exercício, você vai aprender a converter de APIs de callback atuais para usar corrotinas.
Nesta etapa, você vai começar a converter um repositório para usar corrotinas. Para fazer isso, vamos adicionar corrotinas aos ViewModel
, Repository
, Room
e Retrofit
.
É uma boa ideia entender a responsabilidade de cada parte da arquitetura antes de mudar para o uso de corrotinas.
- O
MainDatabase
implementa um banco de dados usando o Room que salva e carrega umTitle
. MainNetwork
implementa uma API de rede que busca um novo título. Ele usa o Retrofit para buscar títulos. ORetrofit
é configurado para retornar erros ou dados simulados aleatoriamente, mas, caso contrário, se comporta como se estivesse fazendo solicitações de rede reais.TitleRepository
implementa uma única API para buscar ou atualizar o título combinando dados da rede e do banco de dados.MainViewModel
representa o estado da tela e processa eventos. Ele vai informar ao repositório para atualizar o título quando o usuário tocar na tela.
Como a solicitação de rede é impulsionada por eventos da interface e queremos iniciar uma corrotina com base neles, o lugar natural para começar a usar corrotinas é no ViewModel
.
A versão do callback
Abra MainViewModel.kt
para ver a declaração de refreshTitle
.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Essa função é chamada sempre que o usuário clica na tela, fazendo com que o repositório atualize o título e grave o novo título no banco de dados.
Essa implementação usa um callback para fazer algumas coisas:
- Antes de iniciar uma consulta, ele mostra um ícone de carregamento com
_spinner.value = true
- Quando ele recebe um resultado, limpa o spinner de carregamento com
_spinner.value = false
. - Se houver um erro, ele vai mostrar um snackbar e limpar o spinner.
O retorno de chamada onCompleted
não recebe o title
. Como gravamos todos os títulos no banco de dados Room
, a interface é atualizada para o título atual observando um LiveData
atualizado por Room
.
Na atualização para corrotinas, vamos manter exatamente o mesmo comportamento. É um bom padrão usar uma fonte de dados observável, como um banco de dados Room
, para manter a interface atualizada automaticamente.
A versão de corrotinas
Vamos reescrever refreshTitle
com corrotinas.
Como vamos precisar dela imediatamente, vamos criar uma função de suspensão vazia no nosso repositório (TitleRespository.kt
). Defina uma nova função que use o operador suspend
para informar ao Kotlin que ela funciona com corrotinas.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Quando terminar este codelab, você vai atualizar isso para usar o Retrofit e a Room para buscar um novo título e gravar no banco de dados usando corrotinas. Por enquanto, ele vai passar 500 milissegundos fingindo trabalhar e depois continuar.
Em MainViewModel
, substitua a versão de callback de refreshTitle
por uma que inicie uma nova corrotina:
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Vamos analisar essa função:
viewModelScope.launch {
Assim como a corrotina para atualizar a contagem de toques, comece iniciando uma nova corrotina em viewModelScope
. Isso vai usar Dispatchers.Main
, o que é aceitável. Mesmo que o refreshTitle
faça uma solicitação de rede e uma consulta de banco de dados, ele pode usar corrotinas para expor uma interface segura para a linha de execução principal. Isso significa que é seguro chamá-lo da linha de execução principal.
Como estamos usando viewModelScope
, quando o usuário sair dessa tela, o trabalho iniciado por essa corrotina será cancelado automaticamente. Isso significa que ele não fará solicitações de rede ou consultas de banco de dados extras.
As próximas linhas de código chamam refreshTitle
no repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Antes de fazer qualquer coisa, essa corrotina inicia o spinner de carregamento e chama refreshTitle
como uma função normal. No entanto, como refreshTitle
é uma função de suspensão, ela é executada de maneira diferente de uma função normal.
Não precisamos transmitir um callback. A corrotina será suspensa até ser retomada por refreshTitle
. Embora pareça uma chamada de função de bloqueio comum, ela aguarda automaticamente até que a rede e a consulta do banco de dados sejam concluídas antes de retomar sem bloquear a linha de execução principal.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
As exceções em funções de suspensão funcionam como erros em funções regulares. Se você gerar um erro em uma função de suspensão, ele será gerado para o autor da chamada. Então, mesmo que sejam executados de maneira bem diferente, você pode usar blocos try/catch regulares para processá-los. Isso é útil porque permite confiar no suporte de linguagem integrado para tratamento de erros em vez de criar um tratamento de erros personalizado para cada callback.
E, se você gerar uma exceção de uma corrotina, ela vai cancelar o pai por padrão. Isso significa que é fácil cancelar várias tarefas relacionadas ao mesmo tempo.
E, por fim, em um bloco "finally", podemos garantir que o spinner sempre seja desativado após a execução da consulta.
Execute o aplicativo novamente selecionando a configuração start e pressionando . Um spinner de carregamento vai aparecer quando você tocar em qualquer lugar. O título vai permanecer o mesmo porque ainda não conectamos nossa rede ou banco de dados.
No próximo exercício, você vai atualizar o repositório para realizar o trabalho.
Neste exercício, você vai aprender a mudar a linha de execução em que uma corrotina é executada para implementar uma versão funcional de TitleRepository
.
Revise o código de callback atual em refreshTitle
Abra TitleRepository.kt
e analise a implementação atual baseada em callback.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
Em TitleRepository.kt
, o método refreshTitleWithCallbacks
é implementado com um callback para comunicar o estado de carregamento e erro ao autor da chamada.
Essa função faz várias coisas para implementar a atualização.
- Mudar para outra conversa com
BACKGROUND
ExecutorService
- Execute a solicitação de rede
fetchNextTitle
usando o método de bloqueioexecute()
. Isso vai executar a solicitação de rede na thread atual, neste caso, uma das threads emBACKGROUND
. - Se o resultado for bem-sucedido, salve-o no banco de dados com
insertTitle
e chame o métodoonCompleted()
. - Se o resultado não for bem-sucedido ou houver uma exceção, chame o método "onError" para informar ao chamador sobre a atualização com falha.
Essa implementação baseada em callback é segura para a linha de execução principal porque não bloqueia a linha de execução principal. No entanto, ele precisa usar um callback para informar ao autor da chamada quando o trabalho for concluído. Ele também chama os callbacks na linha de execução BACKGROUND
para a qual mudou.
Como bloquear chamadas de corrotinas
Sem introduzir corrotinas na rede ou no banco de dados, podemos tornar esse código seguro para a linha de execução principal usando corrotinas. Isso vai nos permitir eliminar o callback e transmitir o resultado de volta para a linha de execução que o chamou inicialmente.
Você pode usar esse padrão sempre que precisar fazer um trabalho de bloqueio ou uso intensivo da CPU em uma corrotina, como classificar e filtrar uma lista grande ou ler do disco.
Para alternar entre qualquer agente, as corrotinas usam withContext
. Chamar withContext
alterna para o outro agente apenas para o lambda e depois volta para o agente que o chamou com o resultado desse lambda.
Por padrão, as corrotinas do Kotlin fornecem três agentes: Main
, IO
e Default
. O agente IO é otimizado para trabalho de E/S, como leitura da rede ou do disco, enquanto o agente Default é otimizado para tarefas que consomem muita CPU.
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Essa implementação usa chamadas de bloqueio para a rede e o banco de dados, mas ainda é um pouco mais simples do que a versão de callback.
Esse código ainda usa chamadas de bloqueio. Chamar execute()
e insertTitle(...)
vai bloquear a linha de execução em que essa corrotina está sendo executada. No entanto, ao mudar para Dispatchers.IO
usando withContext
, bloqueamos uma das linhas de execução no dispatcher de E/S. A corrotina que chamou isso, possivelmente em execução em Dispatchers.Main
, será suspensa até que a lambda withContext
seja concluída.
Em comparação com a versão de callback, há duas diferenças importantes:
withContext
retorna o resultado para o agente que o chamou, neste caso,Dispatchers.Main
. A versão de callback chamava os callbacks em uma linha de execução no serviço de executorBACKGROUND
.- O autor da chamada não precisa transmitir um callback para essa função. Eles podem usar suspender e retomar para receber o resultado ou o erro.
Executar o app novamente
Se você executar o app novamente, verá que a nova implementação baseada em corrotinas está carregando resultados da rede.
Na próxima etapa, você vai integrar corrotinas ao Room e à Retrofit.
Para continuar a integração de corrotinas, vamos usar o suporte para funções de suspensão na versão estável do Room e do Retrofit. Depois, vamos simplificar bastante o código que acabamos de escrever usando as funções de suspensão.
Corrotinas no Room (link em inglês)
Primeiro, abra MainDatabase.kt
e transforme insertTitle
em uma função de suspensão:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Ao fazer isso, a Room vai tornar sua consulta segura para a linha de execução principal e executá-la automaticamente em uma linha de execução em segundo plano. No entanto, isso também significa que você só pode chamar essa consulta de dentro de uma corrotina.
E isso é tudo o que você precisa fazer para usar corrotinas no Room. Muito útil.
Corrotinas no Retrofit (link em inglês)
Em seguida, vamos ver como integrar corrotinas ao Retrofit. Abra MainNetwork.kt
e mude fetchNextTitle
para uma função de suspensão.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Para usar funções de suspensão com o Retrofit, é necessário fazer duas coisas:
- Adicionar um modificador suspend à função
- Remova o wrapper
Call
do tipo de retorno. Aqui, estamos retornandoString
, mas você também pode retornar um tipo complexo com suporte a JSON. Se você ainda quiser fornecer acesso aoResult
completo do retrofit, retorneResult<String>
em vez deString
da função de suspensão.
O Retrofit torna automaticamente as funções de suspensão protegidas para que você possa chamá-las diretamente de Dispatchers.Main
.
Como usar o Room e o Retrofit
Agora que a Room e o Retrofit oferecem suporte a funções de suspensão, podemos usá-las no nosso repositório. Abra TitleRepository.kt
e veja como o uso de funções de suspensão simplifica muito a lógica, mesmo em comparação com a versão de bloqueio:
TítuloRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Uau, isso é muito mais curto. O que aconteceu? Acontece que confiar em suspender e retomar permite que o código seja muito mais curto. A Retrofit permite usar tipos de retorno como String
ou um objeto User
aqui, em vez de um Call
. Isso é seguro porque, dentro da função de suspensão, Retrofit
consegue executar a solicitação de rede em uma linha de execução em segundo plano e retomar a corrotina quando a chamada é concluída.
Melhor ainda, nos livramos do withContext
. Como o Room e o Retrofit fornecem funções de suspensão seguras para a linha de execução principal, é seguro orquestrar esse trabalho assíncrono do Dispatchers.Main
.
Como corrigir erros do compilador
A migração para corrotinas envolve a mudança da assinatura de funções, já que não é possível chamar uma função de suspensão em uma função regular. Quando você adicionou o modificador suspend
nesta etapa, alguns erros do compilador foram gerados, mostrando o que aconteceria se você mudasse uma função para suspender em um projeto real.
Analise o projeto e corrija os erros do compilador mudando a função para suspender a criação. Confira as soluções rápidas para cada uma:
TestingFakes.kt
Atualize os fakes de teste para oferecer suporte aos novos modificadores de suspensão.
TitleDaoFake
- Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.
MainNetworkFake
- Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.
- Substitua
fetchNextTitle
por esta função:
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.
- Substitua
fetchNextTitle
por esta função:
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Exclua a função
refreshTitleWithCallbacks
porque ela não é mais usada.
Executar o app
Execute o app novamente. Depois que ele for compilado, você vai notar que ele está carregando dados usando corrotinas desde o ViewModel até o Room e o Retrofit.
Parabéns! Você trocou completamente este app para usar corrotinas. Para encerrar, vamos falar um pouco sobre como testar o que acabamos de fazer.
Neste exercício, você vai escrever um teste que chama uma função suspend
diretamente.
Como refreshTitle
é exposto como uma API pública, ele será testado diretamente, mostrando como chamar funções de corrotinas em testes.
Esta é a função refreshTitle
que você implementou no último exercício:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Escrever um teste que chama uma função de suspensão
Abra TitleRepositoryTest.kt
na pasta test
, que tem duas tarefas.
Tente chamar refreshTitle
do primeiro teste whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Como refreshTitle
é uma função suspend
, o Kotlin não sabe como chamá-la, exceto em uma corrotina ou outra função de suspensão. Você vai receber um erro do compilador, como "A função de suspensão refreshTitle só pode ser chamada em uma corrotina ou outra função de suspensão".
O executor de testes não sabe nada sobre corrotinas, então não podemos transformar esse teste em uma função de suspensão. Podemos launch
uma corrotina usando um CoroutineScope
, como em um ViewModel
. No entanto, os testes precisam executar as corrotinas até a conclusão antes de retornar. Quando uma função de teste retorna, o teste termina. As corrotinas iniciadas com launch
são códigos assíncronos, que podem ser concluídos em algum momento no futuro. Portanto, para testar esse código assíncrono, você precisa de uma maneira de informar ao teste para esperar até que a corrotina seja concluída. Como launch
é uma chamada não bloqueadora, ela retorna imediatamente e pode continuar executando uma corrotina depois que a função retorna. Ela não pode ser usada em testes. Exemplo:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Esse teste às vezes falha. A chamada para launch
vai retornar imediatamente e ser executada ao mesmo tempo que o restante do caso de teste. O teste não tem como saber se refreshTitle
já foi executado ou não, e qualquer declaração, como verificar se o banco de dados foi atualizado, seria instável. Além disso, se refreshTitle
gerar uma exceção, ela não será gerada na pilha de chamadas de teste. Em vez disso, ele será lançado no gerenciador de exceções não capturadas do GlobalScope
.
A biblioteca kotlinx-coroutines-test
tem a função runBlockingTest
, que fica bloqueada enquanto chama funções de suspensão. Quando runBlockingTest
chama uma função de suspensão ou launches
uma nova corrotina, ela é executada imediatamente por padrão. Pense nela como uma maneira de converter funções de suspensão e corrotinas em chamadas de função normais.
Além disso, o runBlockingTest
vai gerar novamente exceções não capturadas para você. Isso facilita o teste quando uma corrotina gera uma exceção.
Implementar um teste com uma corrotina
Envolva a chamada para refreshTitle
com runBlockingTest
e remova o wrapper GlobalScope.launch
de subject.refreshTitle().
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
Esse teste usa os fakes fornecidos para verificar se "OK" é inserido no banco de dados por refreshTitle
.
Quando o teste chama runBlockingTest
, ele fica bloqueado até que a corrotina iniciada por runBlockingTest
seja concluída. Em seguida, quando chamamos refreshTitle
, ele usa o mecanismo regular de suspensão e retomada para aguardar a adição da linha do banco de dados ao nosso falso.
Depois que a corrotina de teste é concluída, runBlockingTest
retorna.
Criar um teste de tempo limite
Queremos adicionar um tempo limite curto à solicitação de rede. Vamos escrever o teste primeiro e depois implementar o tempo limite. Crie um novo teste:
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
Esse teste usa o MainNetworkCompletableFake
falso fornecido, que é uma simulação de rede projetada para suspender os chamadores até que o teste os continue. Quando refreshTitle
tenta fazer uma solicitação de rede, ele fica parado para sempre porque queremos testar os tempos limite.
Em seguida, ele inicia uma corrotina separada para chamar refreshTitle
. Essa é uma parte fundamental dos testes de tempo limite. O tempo limite precisa ocorrer em uma corrotina diferente daquela que runBlockingTest
cria. Ao fazer isso, podemos chamar a próxima linha, advanceTimeBy(5_000)
, que vai avançar o tempo em 5 segundos e fazer com que a outra corrotina atinja o tempo limite.
Este é um teste de tempo limite completo, e ele será aprovado quando implementarmos o tempo limite.
Execute agora e veja o que acontece:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Um dos recursos do runBlockingTest
é que ele não permite que você vaze corrotinas após a conclusão do teste. Se houver corrotinas não concluídas, como nossa corrotina de inicialização, no final do teste, ele vai falhar.
Adicionar um tempo limite
Abra TitleRepository
e adicione um tempo limite de cinco segundos à busca de rede. Para isso, use a função withTimeout
:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Execute o teste. Ao executar os testes, todos serão aprovados.
No próximo exercício, você vai aprender a escrever funções de ordem superior usando corrotinas.
Neste exercício, você vai refatorar refreshTitle
em MainViewModel
para usar uma função geral de carregamento de dados. Isso vai ensinar você a criar funções de ordem superior que usam corrotinas.
A implementação atual do refreshTitle
funciona, mas podemos criar uma corrotina geral de carregamento de dados que sempre mostre o ícone. Isso pode ser útil em uma base de código que carrega dados em resposta a vários eventos e quer garantir que o ícone de carregamento seja exibido de forma consistente.
Ao analisar a implementação atual, todas as linhas, exceto repository.refreshTitle()
, são clichês para mostrar o spinner e exibir erros.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Como usar corrotinas em funções de ordem superior
Adicione este código a MainViewModel.kt
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Agora refatore refreshTitle()
para usar essa função de ordem superior.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Ao abstrair a lógica de mostrar um ícone de carregamento e erros, simplificamos o código real necessário para carregar dados. Mostrar um spinner ou exibir um erro é algo fácil de generalizar para qualquer carregamento de dados, enquanto a origem e o destino reais precisam ser especificados sempre.
Para criar essa abstração, o launchDataLoad
usa um argumento block
que é uma lambda de suspensão. Um lambda de suspensão permite chamar funções de suspensão. É assim que o Kotlin implementa os builders de corrotinas launch
e runBlocking
que usamos neste codelab.
// suspend lambda
block: suspend () -> Unit
Para criar uma lambda de suspensão, comece com a palavra-chave suspend
. A seta da função e o tipo de retorno Unit
completam a declaração.
Normalmente, não é necessário declarar suas próprias lambdas de suspensão, mas elas podem ser úteis para criar abstrações como essa, que encapsulam a lógica repetida.
Neste exercício, você vai aprender a usar o código baseado em corrotinas do WorkManager.
O que é o WorkManager?
Existem muitas opções no Android para trabalhos em segundo plano adiáveis. Neste exercício, mostramos como integrar o WorkManager com corrotinas. O WorkManager é uma biblioteca compatível, flexível e simples para trabalhos em segundo plano adiáveis. O WorkManager é a solução recomendada para esses casos de uso no Android.
O WorkManager faz parte do Android Jetpack e é um componente de arquitetura para trabalho em segundo plano que requer uma combinação de execução oportunista e garantida. Na execução oportunista, a WorkManager fará o trabalho em segundo plano o quanto antes. Na execução garantida, ela cuidará da lógica para iniciar o trabalho em diversas situações, mesmo se você sair do app.
Por isso, o WorkManager é uma boa opção para tarefas que precisam ser concluídas eventualmente.
Alguns exemplos de tarefas que fazem um bom uso da WorkManager:
- Upload de registros
- Aplicação de filtros a imagens e salvamento da imagem
- Sincronização periódica de dados locais com a rede
Usar corrotinas com o WorkManager
O WorkManager oferece diferentes implementações da classe ListanableWorker
para diferentes casos de uso.
A classe Worker mais simples permite que o WorkManager execute alguma operação síncrona. No entanto, depois de trabalhar para converter nosso codebase para usar corrotinas e funções de suspensão, a melhor maneira de usar o WorkManager é com a classe CoroutineWorker
, que permite definir nossa função doWork()
como uma função de suspensão.
Para começar, abra RefreshMainDataWork
. Ele já estende CoroutineWorker
, e você precisa implementar doWork
.
Dentro da função suspend
doWork
, chame refreshTitle()
do repositório e retorne o resultado adequado.
Depois de concluir o TODO, o código vai ficar assim:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
Observe que CoroutineWorker.doWork()
é uma função de suspensão. Ao contrário da classe Worker
mais simples, esse código NÃO é executado no executor especificado na configuração do WorkManager. Em vez disso, ele usa o dispatcher no membro coroutineContext
(por padrão, Dispatchers.Default
).
Como testar nosso CoroutineWorker
Nenhuma base de código está completa sem testes.
O WorkManager disponibiliza algumas maneiras diferentes de testar suas classes Worker
. Para saber mais sobre a infraestrutura de teste original, leia a documentação.
O WorkManager v2.1 apresenta um novo conjunto de APIs para oferecer uma maneira mais simples de testar classes ListenableWorker
e, consequentemente, o CoroutineWorker. No nosso código, vamos usar uma destas novas APIs: TestListenableWorkerBuilder
.
Para adicionar nosso novo teste, atualize o arquivo RefreshMainDataWorkTest
na pasta androidTest
.
O conteúdo do arquivo é:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Antes de chegar ao teste, informamos WorkManager
sobre a fábrica para que possamos injetar a rede falsa.
O teste usa o TestListenableWorkerBuilder
para criar nosso worker, que pode ser executado chamando o método startWork()
.
O WorkManager é apenas um exemplo de como as corrotinas podem ser usadas para simplificar o design de APIs.
Neste codelab, abordamos os princípios básicos necessários para começar a usar corrotinas no seu app.
Abordamos:
- Como integrar corrotinas a apps Android da interface e trabalhos do WorkManager para simplificar a programação assíncrona.
- Como usar corrotinas em um
ViewModel
para buscar dados da rede e salvar em um banco de dados sem bloquear a linha de execução principal. - E como cancelar todas as corrotinas quando o
ViewModel
for concluído.
Para testar o código baseado em corrotinas, abordamos os dois lados testando o comportamento e chamando diretamente as funções suspend
dos testes.
Saiba mais
Confira o codelab Corrotinas avançadas com Kotlin Flow e LiveData para aprender mais sobre o uso avançado de corrotinas no Android.
As corrotinas do Kotlin têm muitos recursos que não foram abordados neste codelab. Se quiser saber mais sobre as corrotinas do Kotlin, leia os guias de corrotinas (link em inglês) publicados pela JetBrains. Confira também Melhorar o desempenho do app com corrotinas de Kotlin para mais padrões de uso de corrotinas no Android.