Usar corrotinas Kotlin no app Android

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 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á

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

Fazer o download do ZIP

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.

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

  1. MainActivity exibe a IU, registra listeners de clique e pode exibir um Snackbar. Ela transmite eventos para MainViewModel e atualiza a tela com base em LiveData no MainViewModel.
  2. MainViewModel gerencia eventos em onMainViewClicked e se comunicará com MainActivity usando LiveData.
  3. Executors define BACKGROUND, que pode executar itens em uma linha de execução em segundo plano.
  4. 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:

  1. viewModelScope.launch iniciará uma corrotina no viewModelScope. Isso significa que, quando o job que transmitimos para viewModelScope for cancelado, todas as corrotinas nesse job/escopo serão canceladas. Se o usuário sair da atividade antes de delay retornar, essa corrotina será cancelada automaticamente quando onCleared for chamado após a destruição do ViewModel.
  2. Como viewModelScope tem um agente padrão de Dispatchers.Main, essa corrotina será iniciada na linha de execução principal. Veremos como usar linhas de execução diferentes posteriormente.
  3. A função delay é uma função suspend. Ela é mostrada no Android Studio pelo ícone na calha à esquerda. Mesmo que essa corrotina seja executada na linha de execução principal, o delay 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:

  1. InstantTaskExecutorRule é uma regra JUnit que configura 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 teste e que o código use Dispatchers.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

  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, selecione execute.pngRun 'MainViewModelTest'
  3. Para execuções futuras, é possível selecionar essa configuração de teste ao lado do botão execute.png 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.

  1. 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 a Retrofit para buscar títulos. O Retrofit é configurado para retornar aleatoriamente erros ou simular dados, mas, de outra forma, 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. 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 execute.png. 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.

  1. Alternar para outra conversa com BACKGROUND ExecutorService
  2. Execute a solicitação de rede fetchNextTitle usando o método de bloqueio execute(). Isso executará a solicitação de rede na linha de execução atual. Nesse caso, uma das linhas de execução 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 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:

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

  1. Adicionar um modificador de suspensão à função
  2. Remova o wrapper Call do tipo de retorno. Aqui retornamos String, mas você também pode retornar um tipo complexo apoiado por json. Se você ainda quiser fornecer acesso ao Result completo da Retrofit, poderá retornar Result<String> em vez de String 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

  1. Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy

MainNetworkFake (em inglês)

  1. Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy
  2. Substituir fetchNextTitle por esta função
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake (em inglês)

  1. Pressione alt-Enter para adicionar modificadores de suspensão a todas as funções no heiranchy
  2. 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.