1-Usando Kotlin Coroutines No Seu Aplicativo Android
1-Usando Kotlin Coroutines No Seu Aplicativo Android
Introdução
Neste codelab, você aprenderá como usar as Kotlin Coroutines em um aplicativo Android - uma
nova maneira de gerenciar threads em segundo plano que podem simplificar o código, reduzindo
a necessidade de retornos de chamada. As corotinas são um recurso do Kotlin que converte
retornos de chamada assíncronos para tarefas de longa execução, como acesso a banco de dados
ou rede, em código sequencial.
Aqui está um trecho de código para lhe dar uma idéia do que você fará.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Você começará com um aplicativo existente, criado com o Architecture Components, que usa um
estilo de retorno de chamada para tarefas de longa duração.
No final deste codelab, você terá experiência suficiente para usar corotinas em seu aplicativo para
carregar dados da rede e poderá integrar corotinas em um aplicativo. Você também estará
familiarizado com as práticas recomendadas para corotinas e como escrever um teste em relação
ao código que usa corotinas.
2. Como configurar
Faça o download do código
Clique no link a seguir para baixar todo o código deste codelab:
Este aplicativo inicial usa threads para aumentar a contagem um pequeno atraso depois que você
pressiona a tela. Ele também buscará um novo título da rede e o exibirá na tela. Experimente
agora e você verá a contagem e a mensagem mudarem após um pequeno atraso. Neste codelab,
você converterá esse aplicativo para usar corotinas.
Este aplicativo usa componentes de arquitetura para separar o código da interface do usuário em
MainActivity da lógica do aplicativo em MainViewModel. Reserve um momento para se familiarizar
com a estrutura do projeto.
1-MainActivity exibe a interface do usuário, registra ouvintes de clique e pode exibir uma
Snackbar. Ele passa eventos para MainViewModel e atualiza a tela com base no LiveData no
MainViewModel.
2-MainViewModel manipula eventos em onMainViewClicked e se comunicará com MainActivity
usando LiveData.
3-Executors define BACKGROUND, que pode executar coisas em um thread de segundo plano.
Para usar corotinas no Kotlin, você deve incluir a biblioteca coroutines-core no arquivo
build.gradle (Module: app) do seu projeto. Os projetos do codelab já fizeram isso por você,
portanto você não precisa fazer isso para concluir o codelab.
As corotinas no Android estão disponíveis como uma biblioteca principal e extensões específicas
do Android:
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}
Coroutines e RxJava
Se você estiver usando o RxJava na sua base de código atual, poderá integrar-se às corotinas
usando uma biblioteca kotlin-coroutines-rx.
4. Corotinas em Kotlin
No Android, é essencial evitar o bloqueio do thread principal. O segmento principal é um
segmento único que lida com todas as atualizações da interface do usuário. É também o
segmento que chama todos os manipuladores de clique e outros retornos de chamada da
interface do usuário. Como tal, ele deve funcionar sem problemas para garantir uma ótima
experiência do usuário.
Para que seu aplicativo seja exibido ao usuário sem pausas visíveis, o segmento principal precisa
atualizar a tela a cada 16ms ou mais frequentemente, ou seja, cerca de 60 quadros por segundo.
Muitas tarefas comuns levam mais tempo que isso, 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 do thread principal pode fazer com que o aplicativo pause, gagueje ou até congele. E se
você bloquear o thread principal por muito tempo, o aplicativo poderá até falhar e apresentar
uma caixa de diálogo O aplicativo não está respondendo.
Assista ao vídeo abaixo para obter uma introdução sobre como as corotinas resolvem esse
problema para nós no Android, introduzindo a segurança principal.
*** VIDEO
Como esse código é anotado com @UiThread, ele deve ser executado rápido o suficiente para ser
executado no encadeamento 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 levará
segundos ou até minutos para concluir, o thread principal não pode esperar pelo resultado. O
retorno de chamada show (result) permite que slowFetch seja executado em um thread de
segundo plano e retorne o resultado quando estiver pronto.
No final, eles fazem exatamente a mesma coisa: espere até que um resultado esteja disponível em
uma tarefa de longa execução e continue a execução. No entanto, no código, eles parecem muito
diferentes.
O Kotlin possui um método Deferred.await () usado para aguardar o resultado de uma corotina
iniciada com o construtor assíncrono.
No entanto, bloquear códigos como classificar uma lista ou ler um arquivo ainda requer código
explícito para criar segurança principal, mesmo ao usar corotinas. Isso também é verdade se você
estiver usando uma biblioteca de rede ou banco de dados que ainda não suporta corotinas.5.
Controlando a interface do usuário com corotinas
Neste exercício, você escreverá uma rotina para exibir uma mensagem após um atraso. Para
começar, verifique se o módulo é aberto no Android Studio.
Bibliotecas como Room e Retrofit oferecem segurança principal imediata ao usar corotinas, para
que você não precise gerenciar threads para fazer chamadas de rede ou de banco de dados. Isso
geralmente pode levar a um código substancialmente mais simples.
No entanto, bloquear códigos como classificar uma lista ou ler um arquivo ainda requer código
explícito para criar segurança principal, mesmo ao usar corotinas. Isso também é verdade se você
estiver usando uma biblioteca de rede ou banco de dados que ainda não suporta corotinas.
Usando viewModelScope
A biblioteca AndroidX lifecycle-viewmodel-ktx adiciona um CoroutineScope ao ViewModels
configurado para iniciar corotinas relacionadas à interface do usuário. Para usar esta biblioteca,
você deve incluí-la no arquivo build.gradle (Module: app) do seu projeto. Essa etapa já está
concluída nos projetos do codelab.
Usando viewModelScope
A biblioteca AndroidX lifecycle-viewmodel-ktx adiciona um CoroutineScope ao ViewModels
configurado para iniciar corotinas relacionadas à interface do usuário. Para usar esta biblioteca,
você deve incluí-la no arquivo build.gradle (Module: app) do seu projeto. Essa etapa já está
concluída nos projetos do codelab.
dependencies {
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}
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 executar em
um encadeamento em segundo plano. Como o sono bloqueia o encadeamento atual, ele
congelará a interface do usuário se for chamado no encadeamento principal. Um segundo depois
que o usuário clica na visualização principal, ele solicita uma snackbar.
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 rotina que faz a mesma coisa. Você precisará
importar o lançamento e o atraso.
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, aguardando um segundo antes de mostrar uma snackbar. No
entanto, existem algumas diferenças importantes:
A biblioteca de testes kotlinx-coroutines usada nesta seção está marcada como experimental e
pode ter alterações posteriores antes do lançamento.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@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 permitir testar o MainViewModel em um teste fora do dispositivo:
InstantTaskExecutorRule é uma regra JUnit que configura o LiveData para executar cada tarefa de
forma síncrona
MainCoroutineScopeRule é uma regra personalizada nesta base de código que configura
Dispatchers.Main para usar um TestCoroutineDispatcher de kotlinx-coroutines-test. Isso permite
que os testes avancem um relógio virtual para testes e permite que o código use Dispatchers.Main
em testes de unidade.
No método de configuração, uma nova instância do MainViewModel é criada usando testes falsos
- essas são implementações falsas da rede e do banco de dados fornecidos no código inicial para
ajudar a escrever testes sem usar a rede ou o banco de dados real.
Para este teste, as falsificações são necessárias apenas para satisfazer as dependências do
MainViewModel. Posteriormente neste laboratório de código, você atualizará as falsificações para
dar suporte às corotinas.
Escreva um teste que controla corotinas
Adicione um novo teste que garanta que os toques sejam atualizados um segundo após o clique
na visualização principal:
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 corotina que acabamos de criar será lançada. Este teste verifica
se o texto dos toques permanece "0 toques" logo após a chamada onMainViewClicked e, 1
segundo depois, é atualizado para "1 toques".
Esse teste usa o tempo virtual para controlar a execução da corotina iniciada pelo
onMainViewClicked. O MainCoroutineScopeRule permite pausar, retomar ou controlar a execução
de corotinas iniciadas no Dispatchers.Main. Aqui estamos chamando advanceTimeBy (1_000), que
fará com que o expedidor principal execute imediatamente as rotinas que estão agendadas para
serem retomadas 1 segundo depois.
Este teste é totalmente determinístico, o que significa que sempre será executado da mesma
maneira. E, porque ele tem controle total sobre a execução de corotinas lançadas nos Dispatchers.
Principalmente, não é necessário esperar um segundo para que o valor seja definido.
No próximo exercício, você aprenderá como converter de APIs de retorno de chamada existentes
para usar corotinas.
É uma boa idéia entender o que cada parte da arquitetura é responsável antes de passar para o
uso de corotinas.
MainDatabase implementa um banco de dados usando o Room que salva e carrega um Título.
MainNetwork implementa uma API de rede que busca um novo título. Ele usa o Retrofit para
buscar títulos. O retrofit é configurado para retornar erros aleatoriamente ou simular dados, mas
se comporta como se estivesse fazendo solicitações de rede reais.
O 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 lida com eventos. Ele instruirá o repositório a
atualizar o título quando o usuário tocar na tela.
Como a solicitação de rede é direcionada por eventos da interface do usuário e queremos iniciar
uma corotina com base neles, o local natural para começar a usar corotinas é no ViewModel.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
/**
* 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)
}
Essa função é chamada toda vez que o usuário clica na tela - e fará com que o repositório atualize
o título e grave o novo título no banco de dados.
Esta implementação usa um retorno de chamada para fazer algumas coisas:
Antes de iniciar uma consulta, ele exibe um controle giratório de carregamento com
_spinner.value = true
Quando obtém um resultado, limpa o controle giratório de carregamento com _spinner.value =
false
Se ocorrer um erro, ele diz para uma lanchonete exibir e limpa o botão rotativo
Observe que o retorno de chamada onCompleted não recebe o título. Como escrevemos todos os
títulos no banco de dados da sala, a interface do usuário é atualizada para o título atual,
observando um LiveData atualizado pela sala.
Esta é a maneira de criar uma classe anônima no Kotlin. Ele cria um novo objeto que implementa
TitleRefreshCallback.
Como precisaremos imediatamente, vamos criar uma função de suspensão vazia em nosso
repositório (TitleRespository.kt). Defina uma nova função que use o operador de suspensão para
informar ao Kotlin que ele funciona com corotinas.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Quando terminar este codelab, você o atualizará para usar Retrofit e Room para buscar um novo
título e gravá-lo no banco de dados usando corotinas. Por enquanto, gastará apenas 500
milissegundos fingindo fazer o trabalho e continuando.
No MainViewModel, substitua a versão de retorno de chamada de refreshTitle por uma que inicie
uma nova corotina:
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
}
}
}
Ao criar uma corotina a partir de uma não corotina, inicie com o lançamento.
Dessa forma, se eles lançarem uma exceção não capturada, ela será propagada
automaticamente para manipuladores de exceção não capturados (que, por
padrão, travam o aplicativo). Uma corotina iniciada com async não emitirá
uma exceção para o chamador até você ligar em espera. No entanto, você só
pode ligar em espera de dentro de uma corotina, pois é uma função de
suspensão.
Uma vez dentro de uma corotina, você pode usar o launch ou assíncrono para
iniciar corotinas filho. Use o lançamento para quando você não tiver um
resultado para retornar e assíncrono quando o fizer.
Antes dessa corotina fazer qualquer coisa, ele inicia o controle giratório
de carregamento - então chama refreshTitle como uma função regular. No
entanto, como refreshTitle é uma função de suspensão, ele é executado de
maneira diferente da função normal.
Não precisamos passar um retorno de chamada. A corotina será suspensa até
ser retomada por refreshTitle. Embora pareça uma chamada de função de
bloqueio regular, ela aguardará automaticamente até que a consulta à rede e
ao banco de dados seja concluída antes de continuar sem bloquear o segmento
principal.
E, se você lançar uma exceção para fora de uma corotina - essa corotina
cancelará seu pai por padrão. Isso significa que é fácil cancelar várias
tarefas relacionadas juntas.
TitleRepository.kt
// TitleRepository.kt
Você pode usar esse padrão a qualquer momento que precisar executar trabalhos intensivos de
bloqueio ou CPU de dentro de uma rotina, como classificar e filtrar uma lista grande ou ler no
disco.
Esse padrão deve ser usado para integrar APIs de bloqueio no seu código ou para executar um
trabalho intensivo da CPU. Quando possível, é melhor usar funções de suspensão regulares de
bibliotecas como Room ou Retrofit.
Para alternar entre qualquer expedidor, as corotinas usam withContext. Chamar withContext
alterna para o outro distribuidor apenas para o lambda e depois retorna ao distribuidor que o
chamou com o resultado desse lambda.
Por padrão, as rotinas da Kotlin fornecem três Dispatchers: Main, IO e Default. O distribuidor de
E / S é otimizado para o trabalho de E / S, como a leitura da rede ou do disco, enquanto o
distribuidor de E / S é otimizado para tarefas intensivas da 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 que a versão de retorno de chamada.
Esse código ainda usa chamadas de bloqueio. A chamada de execute () e insertTitle (...) bloqueará
o encadeamento em que esta rotina está sendo executada. No entanto, alternando para
Dispatchers.IO usando withContext, estamos bloqueando um dos encadeamentos no dispatcher
de E / S. A corotina que chamou isso, possivelmente executando em Dispatchers.Main, será
suspensa até que o lambda withContext seja concluído.
withContext retorna o resultado para o Dispatcher que o chamou, neste caso Dispatchers.Main. A
versão de retorno de chamada chamou os retornos de chamada em um encadeamento no serviço
do executor BACKGROUND.
O chamador não precisa passar um retorno de chamada para esta função. Eles podem confiar em
suspender e continuar para obter o resultado ou erro.
Dica avançada
Este código não suporta cancelamento de rotina, mas pode! O cancelamento da corotina é
cooperativo. Isso significa que seu código precisa verificar o cancelamento explicitamente, o que
acontece sempre que você chama as funções em kotlinx-coroutines.
Como esse bloco withContext chama apenas chamadas de bloqueio, ele não será cancelado até
que retorne do withContext.
Para consertar isso, você pode chamar o rendimento regularmente para dar uma chance às
outras corotinas e verificar o cancelamento. Aqui você adicionaria uma chamada para gerar entre
a solicitação de rede e a consulta ao banco de dados. Então, se a corotina for cancelada durante a
solicitação de rede, ela não salvará o resultado no banco de dados.
Também é possível verificar o cancelamento explicitamente, o que você deve fazer ao criar
interfaces de corotina de baixo nível.
Corotinas no quarto
Primeiro abra MainDatabase.kt e torne insertTitle 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)
Quando você fizer isso, o Room tornará sua consulta segura principal e executará
automaticamente em um encadeamento em segundo plano. No entanto, isso também significa
que você só pode chamar essa consulta de dentro de uma rotina.
E - é tudo o que você precisa fazer para usar as corotinas no quarto. Muito bacana.
Corotinas em Retrofit
A seguir, vamos ver como integrar corotinas com o Retrofit. Abra MainNetwork.kt e altere
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 o Retrofit, você deve fazer duas coisas:
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)
}
}
Uau, isso é muito mais curto. O que aconteceu? Acontece que confiar em suspender e continuar
permite que o código seja muito menor. O Retrofit nos permite usar tipos de retorno como String
ou um objeto Usuário aqui, em vez de uma Chamada. Isso é seguro, porque, dentro da função de
suspensão, o Retrofit pode executar a solicitação de rede em um encadeamento em segundo
plano e retomar a corotina quando a chamada é concluída.
Melhor ainda, nos livramos do withContext. Como o Room e o Retrofit fornecem funções de
suspensão com segurança principal, é seguro orquestrar esse trabalho assíncrono no
Dispatchers.Main.
Você não precisa usar o withContext para chamar funções de suspensão com segurança principal.
Por convenção, você deve garantir que as funções de suspensão escritas em seu aplicativo sejam
de segurança principal. Dessa forma, é seguro chamá-los de qualquer despachante, até mesmo
Despachantes.
Percorra o projeto e corrija os erros do compilador que alteram a função a suspender criada. Aqui
estão as resoluções rápidas para cada um:
TestingFakes.kt
Atualize as falsificações de teste para suportar os novos modificadores de suspensão.
TítuloDaoFake
Pressione alt-enter e adicione modificadores de suspensão a todas as funções na hierarquia
MainNetworkFake
Pressione alt-enter e adicione modificadores de suspensão a todas as funções na hierarquia
Substitua fetchTitle por esta função
fun fetchTitle() = result
MainNetworkCompletableFake
Pressione alt-enter e adicione modificadores de suspensão a todas as funções
na hierarquia
Substitua fetchTitle por esta função
TitleRepository.kt
Exclua a função refreshTitleWithCallbacks, pois ela não é mais usada.
Execute o aplicativo
Execute o aplicativo novamente, uma vez compilado, você verá que está
carregando dados usando corotinas por todo o caminho, do ViewModel ao Room e
Retrofit!
Como refreshTitle é exposto como uma API pública, ele será testado
diretamente, mostrando como chamar funções de corotinas a partir de testes.
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)
}
}
subject.refreshTitle()
}
Como refreshTitle é uma função de suspensão, o Kotlin não sabe como chamá-
lo, exceto a partir de uma corotina ou outra função de suspensão, e você
receberá um erro do compilador como "Função de suspensão refreshTitle deve
ser chamado apenas de uma corotina ou outra função de suspensão".
O executor de teste não sabe nada sobre corotinas, portanto não podemos
tornar esse teste uma função de suspensão. Poderíamos iniciar uma corotina
usando um CoroutineScope como em um ViewModel, no entanto, os testes
precisam executar corotinas até a conclusão antes de retornarem. Quando uma
função de teste retorna, o teste termina. As corotinas iniciadas com o
lançamento são código assíncrono, que pode ser concluído em algum momento no
futuro. Portanto, para testar esse código assíncrono, é necessário que você
diga ao teste que aguarde até a conclusão da sua rotina. Como o lançamento é
uma chamada sem bloqueio, isso significa que ele retorna imediatamente e
pode continuar executando uma corotina após o retorno da função - não pode
ser usado em testes. Por exemplo:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
Além disso, runBlockingTest retornará exceções não capturadas para você. Isso facilita o teste
quando uma corotina está lançando uma exceção.
Importante: A função runBlockingTest sempre bloqueará o chamador, assim como uma chamada
de função regular. A corotina será executada de forma síncrona no mesmo encadeamento. Você
deve evitar runBlocking e runBlockingTest no código do aplicativo e preferir o lançamento que
retorna imediatamente.
O runBlockingTest deve ser usado apenas em testes, pois executa corotinas de maneira
controlada por testes, enquanto runBlocking pode ser usado para fornecer interfaces de bloqueio
para corotinas.
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")
}
Este teste usa as falsificações fornecidas para verificar se "OK" está inserido no banco de dados
pelo refreshTitle.
Quando o teste chama runBlockingTest, ele será bloqueado até a conclusão da rotina iniciada por
runBlockingTest. Depois, quando chamamos refreshTitle, ele usa o mecanismo regular de
suspensão e retomada para aguardar a adição da linha do banco de dados à nossa falsificação.
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)
}
Este teste usa o MainNetworkCompletableFake falso fornecido, que é um falso de rede projetado
para suspender os chamadores até que o teste os continue. Quando refreshTitle tenta fazer uma
solicitação de rede, ela fica suspensa para sempre, porque queremos testar o tempo limite.
Em seguida, ele lança uma rotina separada para chamar refreshTitle. Essa é uma parte essencial
do tempo limite do teste; o tempo limite deve ocorrer em uma rotina diferente da que o
runBlockingTest cria. Ao fazer isso, podemos chamar a próxima linha, advanceTimeBy (5_000), que
avançará o tempo em 5 segundos e fará com que a outra coroutine atinja o tempo limite.
Este é um teste de tempo limite completo e passará assim que implementarmos o tempo limite.
Um dos recursos do runBlockingTest é que ele não permite vazar corotinas após a conclusão do
teste. Se houver alguma corotina inacabada, como a nossa corotina de lançamento, no final do
teste, ela falhará no teste.
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, você deverá ver todos os testes aprovados!
No próximo exercício, você aprenderá a escrever funções de ordem superior usando corotinas.
Se você não deseja alterar o comportamento das corotinas - por exemplo, em um teste de
integração -, use o runBlocking com as implementações padrão de todos os despachantes.
runBlockingTest é experimental e atualmente possui um erro que faz com que falhe no teste se
uma corotina alternar para um expedidor que executa uma corotina em outro encadeamento.
Não é esperado que o estábulo final tenha esse bug.
11. Usando corotinas em funções de ordem superior
Neste exercício, você refatorará refreshTitle no MainViewModel para usar uma função geral de
carregamento de dados. Isso ensinará como criar funções de ordem superior que usam corotinas.
A implementação atual do refreshTitle funciona, mas podemos criar uma rotina geral de
carregamento de dados que sempre mostra o botão giratório. Isso pode ser útil em uma base de
código que carrega dados em resposta a vários eventos e deseja garantir que o controle giratório
de carregamento seja exibido consistentemente.
Revisando a implementação atual a cada linha, exceto repository.refreshTitle (), é um clichê para
mostrar o girador 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
}
}
}
Por exemplo, você pode declarar um em um adaptador RecyclerView para executar operações
DiffUtil.
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
}
}
}
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
// suspend lambda
Para criar um lambda de suspensão, comece com a palavra-chave suspend. A seta de função e o
tipo de retorno Unidade completam a declaração.
Você não precisa declarar frequentemente suas próprias lambdas de suspensão, mas elas podem
ser úteis para criar abstrações como essa que encapsulam a lógica repetida!
O que é o WorkManager
Existem muitas opções no Android para trabalhos em segundo plano adiados. Este exercício
mostra como integrar o WorkManager às corotinas. O WorkManager é uma biblioteca compatível,
flexível e simples para trabalhos em segundo plano adiados. O WorkManager é a solução
recomendada para esses casos de uso no Android.
Por esse motivo, o WorkManager é uma boa opção para tarefas que devem ser concluídas
eventualmente.
Upload de logs
Aplicando filtros às imagens e salvando a imagem
Sincronizando periodicamente dados locais com a rede
Para saber mais sobre o WorkManager, consulte a documentação.
A classe Worker mais simples nos permite executar alguma operação síncrona pelo
WorkManager. No entanto, tendo trabalhado até agora para converter nossa base de código para
usar corotinas e suspender funções, a melhor maneira de usar o WorkManager é através da
classe CoroutineWorker que permite definir nossa função doWork () como uma função de
suspensão.
Para começar, abra RefreshMainDataWork. Ele já estende o CoroutineWorker e você precisa
implementar o doWork.
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
O WorkManager disponibiliza algumas maneiras diferentes de testar suas classes Worker. Para
saber mais sobre a infraestrutura de teste original, você pode ler a documentação.
O WorkManager v2.1 apresenta um novo conjunto de APIs para oferecer suporte a uma maneira
mais simples de testar as classes ListenableWorker e, como conseqüência, o CoroutineWorker. Em
nosso código, usaremos uma dessas novas API: TestListenableWorkerBuilder.
Para adicionar nosso novo teste, crie um novo arquivo de origem kotlin com o nome
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
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
assertThat(result).isEqualTo(Result.success())
}
Antes de começarmos o teste, informamos o WorkManager sobre a fábrica para injetar a rede
falsa.
O WorkManager é apenas um exemplo de como as corotinas podem ser usadas para simplificar o
design das APIs.
Abordamos como integrar corotinas aos aplicativos Android a partir dos trabalhos de interface do
usuário e WorkManager para simplificar a programação assíncrona. E como usar corotinas dentro
de um ViewModel para buscar dados da rede e salvá-los em um banco de dados sem bloquear o
encadeamento principal, além de como cancelar todas as corotinas quando o ViewModel
terminar.
Para testar o código baseado em rotina, abordamos tanto o comportamento do teste quanto a
chamada direta de funções de suspensão dos testes.
Confira o codelab "Corotinas avançadas com Kotlin Flow e LiveData" para saber mais sobre o uso
avançado de corotinas no Android.
As corotinas Kotlin têm muitos recursos que não foram cobertos por este codelab. Se você estiver
interessado em aprender mais sobre as corotinas Kotlin, leia os guias de corotinas publicados pela
JetBrains. Confira também "Melhorar o desempenho do aplicativo com as corotinas Kotlin" para
obter mais padrões de uso de corotinas no Android.