Usar corrotinas do Kotlin no seu app Android

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 e Room.
  • 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 e runBlocking 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:

Fazer o download do ZIP

… 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.

  1. Se você fez o download do arquivo ZIP kotlin-coroutines, descompacte-o.
  2. Abra o projeto coroutines-codelab no Android Studio.
  3. Selecione o módulo de aplicativo start.
  4. Clique no botão execute.pngRun 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.

  1. MainActivity mostra a interface, registra listeners de clique e pode exibir um Snackbar. Ele transmite eventos para MainViewModel e atualiza a tela com base em LiveData em MainViewModel.
  2. O MainViewModel processa eventos em onMainViewClicked e se comunica com MainActivity usando LiveData..
  3. O Executors define o BACKGROUND,, que pode executar ações em uma linha de execução em segundo plano.
  4. 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:

  1. viewModelScope.launch vai iniciar uma corrotina no viewModelScope. Isso significa que, quando o job transmitido para viewModelScope for cancelado, todas as corrotinas nesse job/escopo também serão canceladas. Se o usuário sair da atividade antes que delay retorne, essa corrotina será cancelada automaticamente quando onCleared for chamada após a destruição do ViewModel.
  2. Como viewModelScope tem um dispatcher padrão de Dispatchers.Main, essa corrotina será iniciada na linha de execução principal. Vamos ver mais tarde como usar diferentes linhas de execução.
  3. A função delay é uma função suspend. Isso é mostrado no Android Studio pelo ícone na 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:

  1. InstantTaskExecutorRule é uma regra do JUnit que configura o LiveData para executar cada tarefa de forma síncrona.
  2. 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 que o código use Dispatchers.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

  1. Clique com o botão direito do mouse no nome da classe MainViewModelTest no editor para abrir um menu de contexto.
  2. No menu de contexto, escolha execute.pngExecutar "MainViewModelTest".
  3. Para execuções futuras, selecione essa configuração de teste nas configurações ao lado do botão execute.png 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.

  1. O MainDatabase implementa um banco de dados usando o Room que salva e carrega um Title.
  2. 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 ou dados simulados aleatoriamente, mas, caso contrário, se comporta como se estivesse fazendo solicitações de rede reais.
  3. TitleRepository implementa uma única API para buscar ou atualizar o título combinando dados da rede e do banco de dados.
  4. 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 execute.png. 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.

  1. Mudar para outra conversa com BACKGROUND ExecutorService
  2. Execute a solicitação de rede fetchNextTitle usando o método de bloqueio execute(). Isso vai executar a solicitação de rede na thread atual, neste caso, uma das threads em BACKGROUND.
  3. Se o resultado for bem-sucedido, salve-o no banco de dados com insertTitle e chame o método onCompleted().
  4. 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:

  1. 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 executor BACKGROUND.
  2. 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:

  1. Adicionar um modificador suspend à função
  2. Remova o wrapper Call do tipo de retorno. Aqui, estamos retornando String, mas você também pode retornar um tipo complexo com suporte a JSON. Se você ainda quiser fornecer acesso ao Result completo do retrofit, retorne Result<String> em vez de String 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

  1. Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.

MainNetworkFake

  1. Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.
  2. Substitua fetchNextTitle por esta função:
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Pressione Alt + Enter para adicionar modificadores de suspensão a todas as funções na hierarquia.
  2. 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.