Neste codelab, você aprenderá a usar corrotinas 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 ao banco de dados ou à rede, em código sequencial.
Veja um snippet de código para dar uma ideia do que você fará.
// 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ê começará com um app já existente, criado com os Componentes de arquitetura, que usam 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 poder integrar corrotinas a um app. Você também conhecerá as práticas recomendadas para corrotinas e como escrever um teste em 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á
- Código de chamada escrito com corrotinas e recebe resultados.
- Use funções de suspensão para tornar o código assíncrono sequencial.
- Use
launch
erunBlocking
para controlar a execução do código. - Aprenda técnicas para converter APIs existentes em corrotinas usando
suspendCoroutine
. - Usar corrotinas com componentes de arquitetura.
- Aprenda as práticas recomendadas para testar corrotinas.
Pré-requisitos
- 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.
Fazer o download do código
Clique no link abaixo para fazer o download de todo o código para este codelab:
Ou clonar o repositório do GitHub pela linha de comando usando o comando abaixo:
$ 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 é 21. A tela de corrotinas Kotlin será exibida:
Este app inicial usa linhas de execução para incrementar a contagem um pequeno atraso depois que você pressionar a tela. Ele também buscará um novo título na rede e o exibirá na tela. Experimente agora. Você verá a contagem e a mensagem mudarem depois de um pequeno atraso. Neste codelab, você converterá esse aplicativo para usar corrotinas.
Esse app usa componentes de arquitetura para separar o código da IU em MainActivity
da lógica do app em MainViewModel
. Familiarize-se com a estrutura do projeto.
MainActivity
exibe a IU, registra listeners de clique e pode exibir umSnackbar
. Ela transmite eventos paraMainViewModel
e atualiza a tela com base emLiveData
noMainViewModel
.MainViewModel
gerencia eventos emonMainViewClicked
e se comunicará comMainActivity
usandoLiveData.
Executors
defineBACKGROUND,
que pode executar itens em uma linha de execução em segundo plano.TitleRepository
busca os resultados da rede e os salva no banco de dados.
Como adicionar corrotinas a um projeto
Para usar corrotinas no Kotlin, inclua a biblioteca coroutines-core
no arquivo build.gradle (Module: app)
do seu projeto. Os projetos do codelab já fizeram isso por você, então não é preciso 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 no Kotlin
- kotlinx-coroutines-android: compatível com a 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, você precisará abrir build.gradle (Module: app)
e adicionar 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, é fundamental evitar o bloqueio da linha de execução principal. A linha de execução principal é uma única linha de execução que processa todas as atualizações na IU. Além disso, é a linha de execução que chama todos os gerenciadores de clique e outros callbacks da IU. Dessa forma, ele precisa ser executado sem problemas para garantir uma ótima experiência do usuário.
Para que o app seja exibido ao usuário sem pausas visíveis, a linha de execução principal precisa atualizar a tela a cada 16 ms ou mais, o que é cerca de 60 quadros por segundo. Muitas tarefas comuns levam mais tempo do que isso, como analisar grandes conjuntos de dados JSON, gravar dados em um banco de dados ou buscar dados da rede. Portanto, chamar códigos como esse na linha de execução principal pode fazer com que o app pause, trave ou mesmo trave. Se você bloquear a linha de execução principal por muito tempo, o app poderá até falhar e apresentar uma caixa de diálogo O app não está respondendo.
Assista ao vídeo abaixo para ver uma introdução sobre como as corrotinas resolvem esse problema para nós no Android com a segurança 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 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.
Veja 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 rapidamente para que a próxima atualização da tela não seja atrasada. No entanto, como o slowFetch
leva alguns segundos ou até minutos para ser concluído, a linha de execução principal não pode aguardar o resultado. O callback show(result)
permite que o slowFetch
seja executado em uma linha de execução em segundo plano e retorne o resultado quando estiver pronto.
Como 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. O código escrito de forma sequencial geralmente é mais fácil de ler e pode até mesmo usar recursos de linguagem, como exceções.
No fim, eles fazem a mesma coisa: esperar até que um resultado esteja disponível em uma tarefa de longa duração e continuar executando. No entanto, eles parecem muito diferentes no código.
A palavra-chave suspend
é uma forma de marcar uma função, ou tipo de função, disponível para corrotinas. Quando uma corrotina chama uma função marcada como suspend
, em vez de bloquear até que ela retorne como uma chamada de função normal, ela suspende a execução até que o resultado esteja pronto e retoma o ponto em que parou com o resultado. Enquanto ele estiver suspenso aguardando um resultado, ele desbloqueará a linha de execução em que está sendo executado 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
. A palavra-chave suspend
funciona como mágica.
Em comparação com o código baseado em callback, o código da corrotina gera 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ê introduzirá corrotinas ao app de exemplo na próxima seção.
Neste exercício, você vai criar uma corrotina para exibir uma mensagem após um atraso. Para começar, abra o módulo start
no Android Studio.
Noções básicas sobre CoroutineScope
Em Kotlin, todas as corrotinas são executadas em um CoroutineScope
(link em inglês). 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 sair de uma Activity
ou Fragment
. Os escopos também permitem especificar um agente padrão. Um agente controla qual linha de execução executa uma corrotina.
Para corrotinas iniciadas pela IU, normalmente é correto iniciá-las em Dispatchers.Main
, que é a linha de execução principal no Android. Uma corrotina iniciada em Dispatchers.Main
não bloqueará a linha de execução principal enquanto ela estiver suspensa. Como uma corrotina ViewModel
quase sempre atualiza a IU na linha de execução principal, iniciar corrotinas na linha de execução principal economiza mais opções. Uma corrotina iniciada na linha de execução principal pode alternar os agentes a qualquer momento depois de ser iniciada. Por exemplo, ele pode usar outro agente para analisar um grande resultado JSON fora da linha de execução principal.
Como usar viewModelScope
A biblioteca AndroidX lifecycle-viewmodel-ktx
adiciona um CoroutineScope aos ViewModels configurados para iniciar corrotinas relacionadas à IU. Para usar essa biblioteca, inclua-a no arquivo build.gradle (Module: start)
do seu projeto. Essa etapa já foi concluída 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
. Este escopo está vinculado a Dispatchers.Main
e será cancelado automaticamente quando a ViewModel
for liberada.
Mudar de linhas de execução para corrotinas
No 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 o sleep
bloqueia a linha de execução atual, ele congelaria a IU se ela fosse chamada na linha de execução principal. Um segundo após o usuário clicar na visualização principal, ele solicita um snackbar.
Para isso, remova o BACKGROUND do código e execute-o novamente. O ícone de carregamento não será exibido e tudo voltará ao 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 esse código baseado em corrotinas 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 o mesmo, aguardando um segundo antes de exibir uma snackbar. No entanto, existem algumas diferenças importantes:
viewModelScope.
launch
iniciará uma corrotina noviewModelScope
. Isso significa que, quando o job que transmitimos paraviewModelScope
for cancelado, todas as corrotinas nesse job/escopo serão canceladas. Se o usuário sair da atividade antes dedelay
retornar, essa corrotina será cancelada automaticamente quandoonCleared
for chamado após a destruição do ViewModel.- Como
viewModelScope
tem um agente padrão deDispatchers.Main
, essa corrotina será iniciada na linha de execução principal. Veremos como usar linhas de execução diferentes posteriormente. - A função
delay
é uma funçãosuspend
. Ela é mostrada no Android Studio pelo ícone na calha à esquerda. Mesmo que essa corrotina seja executada na linha de execução principal, odelay
não bloqueará a linha de execução por um segundo. Em vez disso, o agente programará a corrotina para ser retomada em um segundo na próxima instrução.
Continue e execute o teste. Ao clicar na visualização principal, você verá uma snackbar um segundo depois.
Na próxima seção, veremos como testar essa função.
Neste exercício, você criará um teste para o código que acabou de escrever. Este exercício mostra como testar corrotinas em execução no Dispatchers.Main
usando a biblioteca kotlinx-coroutines-test. Mais adiante neste codelab, você implementará um teste que interage diretamente com corrotinas.
Analise o código existente
Abra MainViewModelTest.kt
na pasta androidTest
.
MainViewModelTest.kt (link em inglês)
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 o código antes e depois da execução de um teste no JUnit. Duas regras são usadas para que possamos testar o MainViewModel em um teste fora do dispositivo:
InstantTaskExecutorRule
é uma regra JUnit que configuraLiveData
para executar cada tarefa de forma síncronaMainCoroutineScopeRule
é 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 teste e que o código useDispatchers.Main
em testes de unidade.
No método setup
, uma nova instância da MainViewModel
é criada usando simulações de teste. Elas são implementações falsas da rede e do banco de dados fornecidas no código inicial para ajudar a escrever testes sem usar o banco de dados ou a rede real.
Neste teste, as falsificações só são necessárias para satisfazer as dependências de MainViewModel
. Mais adiante neste codelab, você atualizará os falsos para oferecer suporte a corrotinas.
Criar um teste que controle corrotinas
Adicione um novo teste que garanta que os toques sejam atualizados um segundo após o clique na visualização principal:
MainViewModelTest.kt (link em inglês)
@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 criamos será iniciada. Esse teste verifica se o texto do toque continua "0 toques" logo após onMainViewClicked
ser chamado e, em seguida, um segundo depois é atualizado para "1 toques"
Esse teste usa tempo virtual para controlar a execução da corrotina iniciada por onMainViewClicked
. O MainCoroutineScopeRule
permite pausar, retomar ou controlar a execução das corrotinas iniciadas no Dispatchers.Main
. Estamos chamando advanceTimeBy(1_000)
, que fará com que o agente principal execute imediatamente as corrotinas programadas para serem retomadas um segundo depois.
Esse teste é totalmente determinístico, ou seja, ele sempre será executado da mesma forma. Além disso, como ele tem controle total sobre a execução das corrotinas iniciadas no Dispatchers.Main
, não precisa aguardar um segundo para que o valor seja definido.
Executar o teste existente
- 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, selecione Run 'MainViewModelTest'
- Para execuções futuras, é possível selecionar essa configuração de teste ao lado do botão na barra de ferramentas. Por padrão, a configuração será chamada MainViewModelTest.
Você verá a aprovação no teste. A execução deve demorar um pouco menos de um segundo.
No próximo exercício, você aprenderá a converter de uma API callback existente para usar corrotinas.
Nesta etapa, você começará a converter um repositório para usar corrotinas. Para isso, adicionaremos corrotinas a ViewModel
, Repository
, Room
e Retrofit
.
É uma boa ideia entender a responsabilidade de cada parte da arquitetura antes de usá-las.
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 a Retrofit para buscar títulos. ORetrofit
é configurado para retornar aleatoriamente erros ou simular dados, mas, de outra forma, 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. O repositório será atualizado quando o usuário tocar na tela.
Como a solicitação de rede é orientada por eventos de IU 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, e isso faz 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 o seguinte:
- Antes de iniciar uma consulta, ele exibe um ícone de carregamento com
_spinner.value = true
. - Quando tiver um resultado, ele limpa o ícone de carregamento com
_spinner.value = false
- Se uma mensagem de erro for exibida, ela informará um snackbar para exibir e limpar o ícone de carregamento
O callback onCompleted
não recebe o title
. Como gravamos todos os títulos no banco de dados Room
, a IU é atualizada para o título atual observando um LiveData
que é atualizado por Room
.
Na atualização das corrotinas, manteremos o mesmo comportamento. Usar um banco de dados observável, como um banco de dados Room
, para manter a IU atualizada automaticamente é um bom padrão.
A versão de corrotinas
Vamos reescrever refreshTitle
com corrotinas.
Como vamos usá-lo 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ê atualizará esse código para usar a Retrofit e a Room para buscar um novo título e gravá-lo no banco de dados usando corrotinas. Por enquanto, serão necessários apenas 500 milissegundos para fazer um trabalho 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, inicie uma nova corrotina no viewModelScope
. Ele usará Dispatchers.Main
, o que está correto. 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 protegida. Isso significa que é seguro chamá-lo usando a linha de execução principal.
Como estamos usando viewModelScope
, quando o usuário sair da 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 adicionais.
As próximas linhas de código chamam refreshTitle
no repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Antes que essa corrotina faça qualquer coisa, ela inicia o ícone de carregamento e chama refreshTitle
como uma função normal. No entanto, como refreshTitle
é uma função de suspensão, ela é executada de forma diferente de uma função normal.
Não é necessário transmitir um retorno de chamada. A corrotina será suspensa até que seja retomada por refreshTitle
. Embora pareça com uma chamada de função de bloqueio normal, ela aguardará automaticamente até que a consulta de rede e banco de dados seja concluída antes de retomar sem bloquear a linha de execução principal.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
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á enviado ao autor da chamada. Assim, mesmo que a execução seja bem diferente, você pode usar blocos try/catch normais para lidar com eles. Isso é útil porque permite que você conte com o suporte de linguagem integrado para a manipulação de erros em vez de criar a manipulação personalizada de erros para cada retorno de chamada.
Se você descartar uma exceção de uma corrotina, essa corrotina cancelará o elemento pai por padrão. Isso significa que é fácil cancelar várias tarefas relacionadas.
Em um bloco final, podemos garantir que o ícone de carregamento está sempre desativado após a consulta ser executada.
Execute o aplicativo novamente selecionando a configuração start e, em seguida, pressionando . Você verá um ícone de carregamento ao tocar em qualquer lugar. O título continuará o mesmo, porque ainda não conectamos nossa rede ou nosso banco de dados.
No próximo exercício, você atualizará o repositório para funcionar.
Neste exercício, você aprenderá a alternar a linha de execução em que uma corrotina é executada para implementar uma versão funcional do TitleRepository
.
Analisar o código de callback existente em refreshTitle
Abra TitleRepository.kt
e analise a implementação atual baseada em callback.
TitleRepository.kt (link em inglês)
// 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))
}
}
}
No método TitleRepository.kt
, o método refreshTitleWithCallbacks
é implementado com um callback para comunicar o estado do carregamento e do erro ao autor da chamada.
Essa função realiza várias ações para implementar a atualização.
- Alternar para outra conversa com
BACKGROUND
ExecutorService
- Execute a solicitação de rede
fetchNextTitle
usando o método de bloqueioexecute()
. Isso executará a solicitação de rede na linha de execução atual. Nesse caso, uma das linhas de execução 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 tiver sido bem-sucedido ou houver uma exceção, chame o método onError para informar o autor da chamada sobre a falha na atualização.
Essa implementação baseada em callback é protegida porque não bloqueia a linha de execução principal. No entanto, ele precisa usar um callback para informar o autor da chamada quando o trabalho for concluído. Ele também chama os callbacks na linha de execução BACKGROUND
que ele trocou.
Chamadas de bloqueio de corrotinas
Sem introduzir as corrotinas na rede ou no banco de dados, podemos tornar esse código protegido usando corrotinas. Isso nos permite eliminar o callback e transmitir o resultado para a linha de execução que o chamou inicialmente.
É possível usar esse padrão sempre que for necessário fazer bloqueios ou trabalhos intensivos da CPU dentro de 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 (link em inglês)
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 o bloqueio de chamadas 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(...)
bloqueará a linha de execução em que essa corrotina está sendo executada. No entanto, ao alternar para Dispatchers.IO
usando withContext
, estamos bloqueando uma das linhas de execução no agente de E/S. A corrotina que chamou isso, possivelmente em execução em Dispatchers.Main
, será suspensa até que o lambda withContext
seja concluído.
Em comparação com a versão de callback, há duas diferenças importantes:
withContext
retorna o resultado para o agente que a chamou, neste caso,Dispatchers.Main
. A versão do callback chamou os callbacks em uma linha de execução no serviço de executorBACKGROUND
.- O autor da chamada não precisa transmitir um retorno de chamada para essa função. Eles podem confiar na suspensão e na retomada para receber o resultado ou erro.
Executar o app novamente
Ao executar o app novamente, você verá que a nova implementação baseada em corrotinas está carregando resultados da rede.
Na próxima etapa, você integrará corrotinas ao Room e à Retrofit.
Para continuar a integração de corrotinas, usaremos a compatibilidade com as funções de suspensão na versão estável do Room e da Retrofit e depois simplificaremos 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 (em inglês)
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Quando você fizer isso, o Room tornará a consulta protegida e a executará 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.
Isso é tudo o que você precisa fazer para usar corrotinas no Room. Muito bonito.
Corrotinas no Retrofit
Agora, vamos ver como integrar corrotinas com a 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 as funções de suspensão com a Retrofit, siga estas duas etapas:
- Adicionar um modificador de suspensão à função
- Remova o wrapper
Call
do tipo de retorno. Aqui retornamosString
, mas você também pode retornar um tipo complexo apoiado por json. Se você ainda quiser fornecer acesso aoResult
completo da Retrofit, poderá retornarResult<String>
em vez deString
a partir da função de suspensão.
A Retrofit tornará as funções de suspensão protegidas automaticamente para que você possa chamá-las diretamente de Dispatchers.Main
.
Como usar o Room e a Retrofit
Agora que o Room e a Retrofit são compatíveis com 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 bastante a lógica, mesmo quando comparado à versão de bloqueio:
Título: Repository.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? A suspensão e a retomada permitem 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, 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 for concluída.
Melhor ainda, removemos o withContext
. Como o Room e a Retrofit oferecem funções de suspensão protegidas, é seguro orquestrar esse trabalho assíncrono a partir de Dispatchers.Main
.
Corrigir erros do compilador
Mover para corrotinas envolve mudar a assinatura de funções, já que não é possível chamar uma função de suspensão de uma função normal. Ao adicionar o modificador suspend
nesta etapa, foram gerados alguns erros do compilador que mostram o que aconteceria se você mudasse uma função para suspender em um projeto real.
Siga o projeto para corrigir os erros do compilador alterando a função para suspensão criada. Veja as soluções rápidas para cada uma:
TestingFakes.kt (link em inglês)
Atualizar as simulações de teste para oferecer compatibilidade com os novos modificadores de suspensão
TitleDaoFake
- Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy
MainNetworkFake (em inglês)
- Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy
- Substituir
fetchNextTitle
por esta função
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake (em inglês)
- Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy
- Substituir
fetchNextTitle
por esta função
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt (link em inglês)
- Exclua a função
refreshTitleWithCallbacks
, porque ela não é mais usada.
Executar o app
Execute o app novamente. Depois da compilação, você verá que ele está carregando dados usando corrotinas do ViewModel para o Room e a Retrofit.
Parabéns! Você mudou totalmente este app para o uso de corrotinas. Para encerrar, vamos falar sobre como testar o que acabamos de fazer.
Neste exercício, você criará um teste que chama uma função suspend
diretamente.
Como o refreshTitle
é exposto como uma API pública, ele será testado diretamente, mostrando como chamar funções de corrotinas de testes.
Veja a função refreshTitle
que você implementou no último exercício:
TitleRepository.kt (link em inglês)
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)
}
}
Criar um teste que chame uma função de suspensão
Abra TitleRepositoryTest.kt
na pasta test
, que tem dois TODOS.
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
que o Kotlin não sabe como chamá-lo, exceto de uma corrotina ou outra função de suspensão, você receberá um erro do compilador, como "Suspend function updateTitle devem ser chamados apenas de uma corrotina ou outra função de suspensão."
O executor de testes não sabe nada sobre corrotinas, então não podemos tornar esse teste uma função de suspensão. Podemos usar launch
em uma corrotina usando um CoroutineScope
, como em um ViewModel
. No entanto, os testes precisam executar as corrotinas até que sejam concluídas antes de retornarem. Quando uma função de teste retorna, o teste acaba. 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 informar ao teste que ele precisa aguardar até que a corrotina seja concluída. Como launch
é uma chamada sem bloqueio, isso significa que ele retorna imediatamente e pode executar 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 falhará. A chamada para launch
retornará imediatamente e será executada ao mesmo tempo que o restante do caso de teste. O teste não tem como saber se o refreshTitle
ainda não foi executado. Qualquer declaração, como verificar se o banco de dados foi atualizado, é instável. E, se refreshTitle
gerar uma exceção, ela não será gerada na pilha de chamadas de teste. Em vez disso, ele será gerado no gerenciador de exceções não capturadas do GlobalScope
.
A biblioteca kotlinx-coroutines-test
tem a função runBlockingTest
que bloqueia 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, runBlockingTest
vai gerar exceções não capturadas para você. Isso facilita o teste quando uma corrotina gera uma exceção.
Implementar um teste com uma corrotina
Una a chamada para refreshTitle
com runBlockingTest
e remova o wrapper GlobalScope.launch
do 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 falsificações fornecidas para verificar se "OK" está inserido no banco de dados por refreshTitle
.
Quando o teste chamar runBlockingTest
, ele será bloqueado até que a corrotina iniciada por runBlockingTest
seja concluída. Em seguida, quando chamamos refreshTitle
, ele usa o mecanismo de suspensão e retomada regular para aguardar a adição da linha do banco de dados à falsa.
Depois que a corrotina de teste for concluída, runBlockingTest
retornará.
Criar um teste de tempo limite
Queremos adicionar um breve tempo limite à solicitação de rede. Primeiro, vamos programar o teste 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 a MainNetworkCompletableFake
fictícia fornecida, que é uma rede falsa criada para suspender os autores de chamadas até que o teste os continue. Quando refreshTitle
tentar fazer uma solicitação de rede, ela ficará parada para sempre porque queremos testar tempos limite.
Em seguida, ele inicia uma corrotina separada para chamar refreshTitle
. Essa é uma parte fundamental dos testes de tempo limite, que precisa acontecer em uma corrotina diferente da que o runBlockingTest
cria. Ao fazer isso, podemos chamar a próxima linha, advanceTimeBy(5_000)
, que avançará em 5 segundos e fará com que a outra corrotina expire.
Este é um teste de tempo limite completo, que será aprovado quando implementarmos o tempo limite.
Execute o teste agora e veja o que acontece:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Um dos recursos do runBlockingTest
é a possibilidade de não vazar corrotinas após a conclusão do teste. Se houver corrotinas incompletas, como a corrotina de inicialização, no final do teste ela será reprovada.
Adicionar um tempo limite
Abra TitleRepository
e adicione um tempo limite de cinco segundos à busca da rede. Para fazer isso, use a função withTimeout
:
TitleRepository.kt (link em inglês)
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. Quando você executar os testes, verá todos eles aprovados.
No próximo exercício, você aprenderá a escrever funções de ordem superior usando corrotinas.
Neste exercício, você refatorará refreshTitle
em MainViewModel
para usar uma função de carregamento de dados geral. Isso ensinará você a criar funções de ordem superior que usam corrotinas.
A implementação atual de refreshTitle
funciona, mas podemos criar uma corrotina de carregamento de dados geral que sempre mostra o ícone de carregamento. 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.
A revisão da implementação atual a cada linha, exceto repository.refreshTitle()
, é padrão para mostrar o ícone de carregamento e os erros de exibição.
// 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
Adicionar este código ao 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 exibição de um ícone de carregamento e mostrar erros, simplificamos nosso código real necessário para carregar dados. Mostrar um ícone de carregamento ou exibir um erro é fácil de generalizar para qualquer carregamento de dados. Já a fonte de dados e o destino precisam ser especificados todas as vezes.
Para criar essa abstração, o launchDataLoad
usa um argumento block
que é um lambda de suspensão. Um lambda de suspensão permite chamar funções de suspensão. É assim que o Kotlin implementa os builders da corrotina launch
e runBlocking
que estamos usando 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
preenchem a declaração.
Muitas vezes, não é preciso declarar os lambdas de suspensão, mas eles podem ser úteis para criar abstrações como essa, que encapsulam a lógica repetida.
Neste exercício, você aprenderá a usar o código baseado em corrotinas do WorkManager.
O que é a WorkManager?
Existem muitas opções no Android para trabalhos em segundo plano adiáveis. Este exercício mostra como integrar o WorkManager com corrotinas. A WorkManager é uma biblioteca compatível, flexível e simples para trabalhos em segundo plano adiáveis. A WorkManager é a solução recomendada para esses casos de uso no Android.
A WorkManager faz parte do Android Jetpack e de 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 no futuro.
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
Como usar corrotinas com o WorkManager
O WorkManager oferece diferentes implementações da classe ListanableWorker
básica para diferentes casos de uso.
A classe Worker mais simples permite realizar algumas operações síncronas no WorkManager. No entanto, após trabalhar para converter nossa base de código para usar corrotinas e suspender funções, a melhor maneira de usar o WorkManager é usar a classe CoroutineWorker
, que permite definir a função doWork()
como uma função de suspensão.
Para começar, abra o app RefreshMainDataWork
. Ela já estende o CoroutineWorker
, e você precisa implementar o doWork
.
Na função suspend
doWork
, chame refreshTitle()
do repositório e retorne o resultado apropriado.
Depois de concluir o TODO, o código 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, mas usa o agente em membro coroutineContext
(por padrão, Dispatchers.Default
).
Como testar nosso CoroutineWorker
Nenhuma base de código deve ser completada sem testes.
O WorkManager disponibiliza algumas maneiras diferentes de testar as classes Worker
, para saber mais sobre a infraestrutura de teste original, leia a documentação.
O WorkManager v2.1 introduz um novo conjunto de APIs para oferecer compatibilidade com uma maneira mais simples de testar classes ListenableWorker
e, como consequência, CoroutineWorker. No código, usaremos uma destas novas APIs: TestListenableWorkerBuilder
.
Para adicionar nosso novo teste, atualize o arquivo RefreshMainDataWorkTest
na pasta androidTest
.
O conteúdo do arquivo é o seguinte:
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 passarmos para o teste, informamos WorkManager
sobre a fábrica para que possamos injetar a rede falsa.
O teste usa o TestListenableWorkerBuilder
para criar o worker e executá-lo chamando o método startWork()
.
O WorkManager é apenas um exemplo de como as corrotinas podem ser usadas para simplificar o design das APIs.
Neste codelab, abordamos os conceitos básicos que você precisará para começar a usar corrotinas no seu app.
Falamos sobre:
- Como integrar corrotinas a apps Android dos jobs da IU e do WorkManager para simplificar a programação assíncrona.
- Como usar corrotinas em um
ViewModel
para buscar dados da rede e salvá-los em um banco de dados sem bloquear a linha de execução principal. - E como cancelar todas as corrotinas quando a
ViewModel
for concluída.
Para testar o código com base em corrotinas, abordamos o comportamento dos testes e como chamar diretamente as funções suspend
dos testes.
Saiba mais
Confira o codelab "Corrotinas avançadas com fluxo do Kotlin e LiveData" para saber como usar corrotinas mais avançadas no Android.
As corrotinas do Kotlin têm muitos recursos que não foram abordados por este codelab. Se você quiser saber mais sobre corrotinas de Kotlin, leia os guias de corrotinas publicados pela JetBrains. Confira também Melhorar o desempenho do app com corrotinas de Kotlin (link em inglês) para ver mais padrões de uso de corrotinas no Android.